From 2244743545e8e5a75b4cce219605588cd29581b1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 12 Apr 2017 21:56:27 +0800 Subject: [PATCH 01/36] convert `AST_Seq` from binary tree to array (#1460) - rename `AST_Seq` to `AST_Sequence` - raise default sequences_limit from 200 to 800 --- bin/extract-props.js | 4 +- bin/uglifyjs | 4 +- lib/ast.js | 64 +----- lib/compress.js | 371 +++++++++++++++++++--------------- lib/mozilla-ast.js | 10 +- lib/output.js | 23 ++- lib/parse.js | 19 +- lib/propmangle.js | 9 +- lib/transform.js | 5 +- lib/utils.js | 2 +- test/compress/conditionals.js | 24 +-- test/compress/evaluate.js | 24 +-- test/compress/issue-640.js | 6 +- test/compress/sequences.js | 118 +++++++++-- 14 files changed, 376 insertions(+), 307 deletions(-) diff --git a/bin/extract-props.js b/bin/extract-props.js index a5b61458..7ce7d31e 100755 --- a/bin/extract-props.js +++ b/bin/extract-props.js @@ -50,8 +50,8 @@ function getProps(filename) { try { (function walk(node){ node.walk(new U2.TreeWalker(function(node){ - if (node instanceof U2.AST_Seq) { - walk(node.cdr); + if (node instanceof U2.AST_Sequence) { + walk(node.expressions[node.expressions.length - 1]); return true; } if (node instanceof U2.AST_String) { diff --git a/bin/uglifyjs b/bin/uglifyjs index 635ca365..ef776492 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -569,7 +569,7 @@ function getOptions(flag, constants) { } } ast.walk(new UglifyJS.TreeWalker(function(node){ - if (node instanceof UglifyJS.AST_Seq) return; // descend + if (node instanceof UglifyJS.AST_Sequence) return; // descend if (node instanceof UglifyJS.AST_Assign) { var name = node.left.print_to_string().replace(/-/g, "_"); var value = node.right; @@ -583,7 +583,7 @@ function getOptions(flag, constants) { ret[name] = true; return true; // no descend } - print_error(node.TYPE) + print_error(node.TYPE); print_error("Error parsing arguments for flag `" + flag + "': " + x); process.exit(1); })); diff --git a/lib/ast.js b/lib/ast.js index ba1330f4..f78ac576 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -613,68 +613,16 @@ var AST_New = DEFNODE("New", null, { $documentation: "An object instantiation. Derives from a function call since it has exactly the same properties" }, AST_Call); -var AST_Seq = DEFNODE("Seq", "car cdr", { - $documentation: "A sequence expression (two comma-separated expressions)", +var AST_Sequence = DEFNODE("Sequence", "expressions", { + $documentation: "A sequence expression (comma-separated expressions)", $propdoc: { - car: "[AST_Node] first element in sequence", - cdr: "[AST_Node] second element in sequence" - }, - $cons: function(x, y) { - var seq = new AST_Seq(x); - seq.car = x; - seq.cdr = y; - return seq; - }, - $from_array: function(array) { - if (array.length == 0) return null; - if (array.length == 1) return array[0].clone(); - var list = null; - for (var i = array.length; --i >= 0;) { - list = AST_Seq.cons(array[i], list); - } - var p = list; - while (p) { - if (p.cdr && !p.cdr.cdr) { - p.cdr = p.cdr.car; - break; - } - p = p.cdr; - } - return list; - }, - to_array: function() { - var p = this, a = []; - while (p) { - a.push(p.car); - if (p.cdr && !(p.cdr instanceof AST_Seq)) { - a.push(p.cdr); - break; - } - p = p.cdr; - } - return a; - }, - add: function(node) { - var p = this; - while (p) { - if (!(p.cdr instanceof AST_Seq)) { - var cell = AST_Seq.cons(p.cdr, node); - return p.cdr = cell; - } - p = p.cdr; - } - }, - len: function() { - if (this.cdr instanceof AST_Seq) { - return this.cdr.len() + 1; - } else { - return 2; - } + expressions: "[AST_Node*] array of expressions (at least two)" }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.car._walk(visitor); - if (this.cdr) this.cdr._walk(visitor); + this.expressions.forEach(function(node) { + node._walk(visitor); + }); }); } }); diff --git a/lib/compress.js b/lib/compress.js index 1d9258cf..8c254573 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -111,7 +111,7 @@ function Compressor(options, false_by_default) { }; } var sequences = this.options["sequences"]; - this.sequences_limit = sequences == 1 ? 200 : sequences | 0; + this.sequences_limit = sequences == 1 ? 800 : sequences | 0; this.warnings_produced = {}; }; @@ -440,6 +440,13 @@ merge(Compressor.prototype, { return new ctor(props); }; + function make_sequence(orig, expressions) { + if (expressions.length == 1) return expressions[0]; + return make_node(AST_Sequence, orig, { + expressions: expressions + }); + } + function make_node_from_constant(val, orig) { switch (typeof val) { case "string": @@ -482,16 +489,19 @@ merge(Compressor.prototype, { if (parent instanceof AST_UnaryPrefix && parent.operator == "delete" || parent instanceof AST_Call && parent.expression === orig && (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name == "eval")) { - return make_node(AST_Seq, orig, { - car: make_node(AST_Number, orig, { - value: 0 - }), - cdr: val - }); + return make_sequence(orig, [ make_node(AST_Number, orig, { value: 0 }), val ]); } return val; } + function merge_sequence(array, node) { + if (node instanceof AST_Sequence) { + array.push.apply(array, node.expressions); + } else { + array.push(node); + } + } + function as_statement_array(thing) { if (thing === null) return []; if (thing instanceof AST_BlockStatement) return thing.body; @@ -1000,18 +1010,17 @@ merge(Compressor.prototype, { if (statements.length < 2) return statements; var seq = [], ret = []; function push_seq() { - seq = AST_Seq.from_array(seq); - if (seq) ret.push(make_node(AST_SimpleStatement, seq, { - body: seq - })); + if (!seq.length) return; + var body = make_sequence(seq[0], seq); + ret.push(make_node(AST_SimpleStatement, body, { body: body })); seq = []; }; statements.forEach(function(stat){ if (stat instanceof AST_SimpleStatement) { - if (seqLength(seq) >= compressor.sequences_limit) push_seq(); + if (seq.length >= compressor.sequences_limit) push_seq(); var body = stat.body; if (seq.length > 0) body = body.drop_side_effect_free(compressor); - if (body) seq.push(body); + if (body) merge_sequence(seq, body); } else { push_seq(); ret.push(stat); @@ -1023,27 +1032,16 @@ merge(Compressor.prototype, { return ret; }; - function seqLength(a) { - for (var len = 0, i = 0; i < a.length; ++i) { - var stat = a[i]; - if (stat instanceof AST_Seq) { - len += stat.len(); - } else { - len++; - } - } - return len; - }; - function sequencesize_2(statements, compressor) { function cons_seq(right) { ret.pop(); var left = prev.body; - if (left instanceof AST_Seq) { - left.add(right); - } else { - left = AST_Seq.cons(left, right); + if (!(left instanceof AST_Sequence)) { + left = make_node(AST_Sequence, left, { + expressions: [ left ] + }); } + merge_sequence(left.expressions, right); return left.transform(compressor); }; var ret = [], prev = null; @@ -1202,8 +1200,8 @@ merge(Compressor.prototype, { return this.consequent._eq_null(pure_getters) || this.alternative._eq_null(pure_getters); }) - def(AST_Seq, function(pure_getters) { - return this.cdr._eq_null(pure_getters); + def(AST_Sequence, function(pure_getters) { + return this.expressions[this.expressions.length - 1]._eq_null(pure_getters); }); def(AST_SymbolRef, function(pure_getters) { if (this.is_undefined) return true; @@ -1236,8 +1234,8 @@ merge(Compressor.prototype, { def(AST_Assign, function(){ return this.operator == "=" && this.right.is_boolean(); }); - def(AST_Seq, function(){ - return this.cdr.is_boolean(); + def(AST_Sequence, function(){ + return this.expressions[this.expressions.length - 1].is_boolean(); }); def(AST_True, return_true); def(AST_False, return_true); @@ -1263,8 +1261,8 @@ merge(Compressor.prototype, { return binary(this.operator.slice(0, -1)) || this.operator == "=" && this.right.is_number(compressor); }); - def(AST_Seq, function(compressor){ - return this.cdr.is_number(compressor); + def(AST_Sequence, function(compressor){ + return this.expressions[this.expressions.length - 1].is_number(compressor); }); def(AST_Conditional, function(compressor){ return this.consequent.is_number(compressor) && this.alternative.is_number(compressor); @@ -1287,8 +1285,8 @@ merge(Compressor.prototype, { def(AST_Assign, function(compressor){ return (this.operator == "=" || this.operator == "+=") && this.right.is_string(compressor); }); - def(AST_Seq, function(compressor){ - return this.cdr.is_string(compressor); + def(AST_Sequence, function(compressor){ + return this.expressions[this.expressions.length - 1].is_string(compressor); }); def(AST_Conditional, function(compressor){ return this.consequent.is_string(compressor) && this.alternative.is_string(compressor); @@ -1616,10 +1614,10 @@ merge(Compressor.prototype, { return this.expression; return basic_negation(this); }); - def(AST_Seq, function(compressor){ - var self = this.clone(); - self.cdr = self.cdr.negate(compressor); - return self; + def(AST_Sequence, function(compressor){ + var expressions = this.expressions.slice(); + expressions.push(expressions.pop().negate(compressor)); + return make_sequence(this, expressions); }); def(AST_Conditional, function(compressor, first_in_statement){ var self = this.clone(); @@ -1763,9 +1761,10 @@ merge(Compressor.prototype, { || this.expression.has_side_effects(compressor) || this.property.has_side_effects(compressor); }); - def(AST_Seq, function(compressor){ - return this.car.has_side_effects(compressor) - || this.cdr.has_side_effects(compressor); + def(AST_Sequence, function(compressor){ + return this.expressions.some(function(expression, index) { + return expression.has_side_effects(compressor); + }); }); })(function(node, func){ node.DEFMETHOD("has_side_effects", func); @@ -2015,12 +2014,12 @@ merge(Compressor.prototype, { for (var i = 0; i < def.length;) { var x = def[i]; if (x._unused_side_effects) { - side_effects.push(x._unused_side_effects); + merge_sequence(side_effects, x._unused_side_effects); def.splice(i, 1); } else { if (side_effects.length > 0) { - side_effects.push(x.value); - x.value = AST_Seq.from_array(side_effects); + merge_sequence(side_effects, x.value); + x.value = make_sequence(x.value, side_effects); side_effects = []; } ++i; @@ -2029,7 +2028,7 @@ merge(Compressor.prototype, { if (side_effects.length > 0) { side_effects = make_node(AST_BlockStatement, node, { body: [ make_node(AST_SimpleStatement, node, { - body: AST_Seq.from_array(side_effects) + body: make_sequence(node, side_effects) }) ] }); } else { @@ -2179,8 +2178,8 @@ merge(Compressor.prototype, { self.body.splice(i, 1); continue; } - if (expr instanceof AST_Seq - && (assign = expr.car) instanceof AST_Assign + if (expr instanceof AST_Sequence + && (assign = expr.expressions[0]) instanceof AST_Assign && assign.operator == "=" && (sym = assign.left) instanceof AST_Symbol && vars.has(sym.name)) @@ -2190,7 +2189,7 @@ merge(Compressor.prototype, { def.value = assign.right; remove(defs, def); defs.push(def); - self.body[i].body = expr.cdr; + self.body[i].body = make_sequence(expr, expr.expressions.slice(1)); continue; } } @@ -2224,12 +2223,14 @@ merge(Compressor.prototype, { // if all elements were dropped. Note: original array may be // returned if nothing changed. function trim(nodes, compressor, first_in_statement) { + var len = nodes.length; + if (!len) return null; var ret = [], changed = false; - for (var i = 0, len = nodes.length; i < len; i++) { + for (var i = 0; i < len; i++) { var node = nodes[i].drop_side_effect_free(compressor, first_in_statement); changed |= node !== nodes[i]; if (node) { - ret.push(node); + merge_sequence(ret, node); first_in_statement = false; } } @@ -2254,7 +2255,7 @@ merge(Compressor.prototype, { this.pure.value = this.pure.value.replace(/[@#]__PURE__/g, ' '); } var args = trim(this.args, compressor, first_in_statement); - return args && AST_Seq.from_array(args); + return args && make_sequence(this, args); }); def(AST_Function, return_null); def(AST_Binary, function(compressor, first_in_statement){ @@ -2270,10 +2271,7 @@ merge(Compressor.prototype, { default: var left = this.left.drop_side_effect_free(compressor, first_in_statement); if (!left) return this.right.drop_side_effect_free(compressor, first_in_statement); - return make_node(AST_Seq, this, { - car: left, - cdr: right - }); + return make_sequence(this, [ left, right ]); } }); def(AST_Assign, return_this); @@ -2316,14 +2314,14 @@ merge(Compressor.prototype, { }); def(AST_Object, function(compressor, first_in_statement){ var values = trim(this.properties, compressor, first_in_statement); - return values && AST_Seq.from_array(values); + return values && make_sequence(this, values); }); def(AST_ObjectProperty, function(compressor, first_in_statement){ return this.value.drop_side_effect_free(compressor, first_in_statement); }); def(AST_Array, function(compressor, first_in_statement){ var values = trim(this.elements, compressor, first_in_statement); - return values && AST_Seq.from_array(values); + return values && make_sequence(this, values); }); def(AST_Dot, function(compressor, first_in_statement){ if (this.expression.may_eq_null(compressor)) return this; @@ -2335,19 +2333,15 @@ merge(Compressor.prototype, { if (!expression) return this.property.drop_side_effect_free(compressor, first_in_statement); var property = this.property.drop_side_effect_free(compressor); if (!property) return expression; - return make_node(AST_Seq, this, { - car: expression, - cdr: property - }); + return make_sequence(this, [ expression, property ]); }); - def(AST_Seq, function(compressor){ - var cdr = this.cdr.drop_side_effect_free(compressor); - if (cdr === this.cdr) return this; - if (!cdr) return this.car; - return make_node(AST_Seq, this, { - car: this.car, - cdr: cdr - }); + def(AST_Sequence, function(compressor){ + var last = this.expressions[this.expressions.length - 1]; + var expr = last.drop_side_effect_free(compressor); + if (expr === last) return this; + var expressions = this.expressions.slice(0, -1); + if (expr) merge_sequence(expressions, expr); + return make_sequence(this, expressions); }); })(function(node, func){ node.DEFMETHOD("drop_side_effect_free", func); @@ -2737,7 +2731,7 @@ merge(Compressor.prototype, { return a; }, []); if (assignments.length == 0) return null; - return AST_Seq.from_array(assignments); + return make_sequence(this, assignments); }); OPT(AST_Definitions, function(self, compressor){ @@ -2979,12 +2973,12 @@ merge(Compressor.prototype, { var value = exp.body[0].value; if (!value || value.is_constant()) { var args = self.args.concat(value || make_node(AST_Undefined, self)); - return AST_Seq.from_array(args).transform(compressor); + return make_sequence(self, args).transform(compressor); } } if (compressor.option("side_effects") && all(exp.body, is_empty)) { var args = self.args.concat(make_node(AST_Undefined, self)); - return AST_Seq.from_array(args).transform(compressor); + return make_sequence(self, args).transform(compressor); } } if (compressor.option("drop_console")) { @@ -3025,40 +3019,85 @@ merge(Compressor.prototype, { return self; }); - OPT(AST_Seq, function(self, compressor){ - if (!compressor.option("side_effects")) + OPT(AST_Sequence, function(self, compressor){ + if (!compressor.option("side_effects")) return self; + var expressions = []; + filter_for_side_effects(); + var end = expressions.length - 1; + trim_right_for_undefined(); + if (end > 0 && compressor.option("cascade")) trim_left_for_assignment(); + if (end == 0) { + self = maintain_this_binding(compressor.parent(), self, expressions[0]); + if (!(self instanceof AST_Sequence)) self = self.optimize(compressor); return self; - self.car = self.car.drop_side_effect_free(compressor, first_in_statement(compressor)); - if (!self.car) return maintain_this_binding(compressor.parent(), self, self.cdr); - if (compressor.option("cascade")) { - var left; - if (self.car instanceof AST_Assign - && !self.car.left.has_side_effects(compressor)) { - left = self.car.left; - } else if (self.car instanceof AST_Unary - && (self.car.operator == "++" || self.car.operator == "--")) { - left = self.car.expression; + } + self.expressions = expressions; + return self; + + function filter_for_side_effects() { + var first = first_in_statement(compressor); + var last = self.expressions.length - 1; + self.expressions.forEach(function(expr, index) { + if (index < last) expr = expr.drop_side_effect_free(compressor, first); + if (expr) { + merge_sequence(expressions, expr); + first = false; + } + }); + } + + function trim_right_for_undefined() { + while (end > 0 && is_undefined(expressions[end], compressor)) end--; + if (end < expressions.length - 1) { + expressions[end] = make_node(AST_UnaryPrefix, self, { + operator : "void", + expression : expressions[end] + }); + expressions.length = end + 1; } - if (left - && !(left instanceof AST_SymbolRef - && left.definition().orig[0] instanceof AST_SymbolLambda)) { - var parent, field; - var cdr = self.cdr; + } + + function trim_left_for_assignment() { + for (var i = 0, j = 1; j <= end; j++) { + var left = expressions[i]; + var cdr = expressions[j]; + if (left instanceof AST_Assign + && !left.left.has_side_effects(compressor)) { + left = left.left; + } else if (left instanceof AST_Unary + && (left.operator == "++" || left.operator == "--")) { + left = left.expression; + } else left = null; + if (!left || + left instanceof AST_SymbolRef + && left.definition().orig[0] instanceof AST_SymbolLambda) { + expressions[++i] = cdr; + continue; + } + var parent = null, field; while (true) { if (cdr.equivalent_to(left)) { - var car = self.car instanceof AST_UnaryPostfix ? make_node(AST_UnaryPrefix, self.car, { - operator: self.car.operator, - expression: left - }) : self.car; + var car = expressions[i]; + if (car instanceof AST_UnaryPostfix) { + car = make_node(AST_UnaryPrefix, car, { + operator: car.operator, + expression: left + }); + } if (parent) { parent[field] = car; - return self.cdr; + expressions[i] = expressions[j]; + } else { + expressions[i] = car; } - return car; + break; } if (cdr instanceof AST_Binary && !(cdr instanceof AST_Assign)) { if (cdr.left.is_constant()) { - if (cdr.operator == "||" || cdr.operator == "&&") break; + if (cdr.operator == "||" || cdr.operator == "&&") { + expressions[++i] = expressions[j]; + break; + } field = "right"; } else { field = "left"; @@ -3066,31 +3105,27 @@ merge(Compressor.prototype, { } else if (cdr instanceof AST_Call || cdr instanceof AST_Unary && !unary_side_effects(cdr.operator)) { field = "expression"; - } else break; + } else { + expressions[++i] = expressions[j]; + break; + } parent = cdr; cdr = cdr[field]; } } + end = i; + expressions.length = end + 1; } - if (is_undefined(self.cdr, compressor)) { - return make_node(AST_UnaryPrefix, self, { - operator : "void", - expression : self.car - }); - } - return self; }); AST_Unary.DEFMETHOD("lift_sequences", function(compressor){ if (compressor.option("sequences")) { - if (this.expression instanceof AST_Seq) { - var seq = this.expression; - var x = seq.to_array(); + if (this.expression instanceof AST_Sequence) { + var x = this.expression.expressions.slice(); var e = this.clone(); e.expression = x.pop(); x.push(e); - seq = AST_Seq.from_array(x).transform(compressor); - return seq; + return make_sequence(this, x).optimize(compressor); } } return this; @@ -3108,15 +3143,12 @@ merge(Compressor.prototype, { || e instanceof AST_NaN || e instanceof AST_Infinity || e instanceof AST_Undefined)) { - if (e instanceof AST_Seq) { - e = e.to_array(); + if (e instanceof AST_Sequence) { + e = e.expressions.slice(); e.push(make_node(AST_True, self)); - return AST_Seq.from_array(e).optimize(compressor); + return make_sequence(self, e).optimize(compressor); } - return make_node(AST_Seq, self, { - car: e, - cdr: make_node(AST_True, self) - }).optimize(compressor); + return make_sequence(self, [ e, make_node(AST_True, self) ]).optimize(compressor); } var seq = self.lift_sequences(compressor); if (seq !== self) { @@ -3146,10 +3178,10 @@ merge(Compressor.prototype, { // typeof always returns a non-empty string, thus it's // always true in booleans compressor.warn("Boolean expression always true [{file}:{line},{col}]", self.start); - return (e instanceof AST_SymbolRef ? make_node(AST_True, self) : make_node(AST_Seq, self, { - car: e, - cdr: make_node(AST_True, self) - })).optimize(compressor); + return (e instanceof AST_SymbolRef ? make_node(AST_True, self) : make_sequence(self, [ + e, + make_node(AST_True, self) + ])).optimize(compressor); } } if (self.operator == "-" && e instanceof AST_Infinity) { @@ -3181,29 +3213,32 @@ merge(Compressor.prototype, { AST_Binary.DEFMETHOD("lift_sequences", function(compressor){ if (compressor.option("sequences")) { - if (this.left instanceof AST_Seq) { - var seq = this.left; - var x = seq.to_array(); + if (this.left instanceof AST_Sequence) { + var x = this.left.expressions.slice(); var e = this.clone(); e.left = x.pop(); x.push(e); - return AST_Seq.from_array(x).optimize(compressor); + return make_sequence(this, x).optimize(compressor); } - if (this.right instanceof AST_Seq && !this.left.has_side_effects(compressor)) { + if (this.right instanceof AST_Sequence && !this.left.has_side_effects(compressor)) { var assign = this.operator == "=" && this.left instanceof AST_SymbolRef; - var root = this.right.clone(); - var cursor, seq = root; - while (assign || !seq.car.has_side_effects(compressor)) { - cursor = seq; - if (seq.cdr instanceof AST_Seq) { - seq = seq.cdr = seq.cdr.clone(); - } else break; + var x = this.right.expressions; + var last = x.length - 1; + for (var i = 0; i < last; i++) { + if (!assign && x[i].has_side_effects(compressor)) break; } - if (cursor) { + if (i == last) { + x = x.slice(); var e = this.clone(); - e.right = cursor.cdr; - cursor.cdr = e; - return root.optimize(compressor); + e.right = x.pop(); + x.push(e); + return make_sequence(this, x).optimize(compressor); + } else if (i > 0) { + var e = this.clone(); + e.right = make_sequence(this.right, x.slice(i)); + x = x.slice(0, i); + x.push(e); + return make_sequence(this, x).optimize(compressor); } } } @@ -3272,17 +3307,17 @@ merge(Compressor.prototype, { var rr = self.right.evaluate(compressor); 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); + return make_sequence(self, [ + self.right, + make_node(AST_True, self) + ]).optimize(compressor); } 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, - cdr: make_node(AST_True, self) - }).optimize(compressor); + return make_sequence(self, [ + self.left, + make_node(AST_True, self) + ]).optimize(compressor); } } if (compressor.option("comparisons") && self.is_boolean()) { @@ -3336,10 +3371,10 @@ merge(Compressor.prototype, { var rr = self.right.evaluate(compressor); if (!rr) { compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { - car: self.left, - cdr: make_node(AST_False, self) - }).optimize(compressor); + return make_sequence(self, [ + self.left, + make_node(AST_False, self) + ]).optimize(compressor); } else if (rr !== self.right) { compressor.warn("Dropping side-effect-free && in boolean context [{file}:{line},{col}]", self.start); return self.left.optimize(compressor); @@ -3362,10 +3397,10 @@ merge(Compressor.prototype, { return self.left.optimize(compressor); } else if (rr !== self.right) { compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); - return make_node(AST_Seq, self, { - car: self.left, - cdr: make_node(AST_True, self) - }).optimize(compressor); + return make_sequence(self, [ + self.left, + make_node(AST_True, self) + ]).optimize(compressor); } } break; @@ -3709,10 +3744,12 @@ merge(Compressor.prototype, { OPT(AST_Conditional, function(self, compressor){ if (!compressor.option("conditionals")) return self; - if (self.condition instanceof AST_Seq) { - var car = self.condition.car; - self.condition = self.condition.cdr; - return AST_Seq.cons(car, self); + // This looks like lift_sequences(), should probably be under "sequences" + if (self.condition instanceof AST_Sequence) { + var expressions = self.condition.expressions.slice(); + self.condition = expressions.pop(); + expressions.push(self); + return make_sequence(self, expressions); } var cond = self.condition.evaluate(compressor); if (cond !== self.condition) { @@ -3795,10 +3832,10 @@ merge(Compressor.prototype, { } // x ? y : y --> x, y if (consequent.equivalent_to(alternative)) { - return make_node(AST_Seq, self, { - car: self.condition, - cdr: consequent - }).optimize(compressor); + return make_sequence(self, [ + self.condition, + consequent + ]).optimize(compressor); } if (is_true(self.consequent)) { @@ -3968,10 +4005,10 @@ merge(Compressor.prototype, { function literals_in_boolean_context(self, compressor) { if (compressor.option("booleans") && compressor.in_boolean_context()) { - return best_of(compressor, self, make_node(AST_Seq, self, { - car: self, - cdr: make_node(AST_True, self) - }).optimize(compressor)); + return best_of(compressor, self, make_sequence(self, [ + self, + make_node(AST_True, self) + ]).optimize(compressor)); } return self; }; diff --git a/lib/mozilla-ast.js b/lib/mozilla-ast.js index 12b55dc5..e97d6191 100644 --- a/lib/mozilla-ast.js +++ b/lib/mozilla-ast.js @@ -149,7 +149,11 @@ }); }, SequenceExpression: function(M) { - return AST_Seq.from_array(M.expressions.map(from_moz)); + return new AST_Sequence({ + start : my_start_token(M), + end : my_end_token(M), + expressions: M.expressions.map(from_moz) + }); }, MemberExpression: function(M) { return new (M.computed ? AST_Sub : AST_Dot)({ @@ -332,10 +336,10 @@ }; }); - def_to_moz(AST_Seq, function To_Moz_SequenceExpression(M) { + def_to_moz(AST_Sequence, function To_Moz_SequenceExpression(M) { return { type: "SequenceExpression", - expressions: M.to_array().map(to_moz) + expressions: M.expressions.map(to_moz) }; }); diff --git a/lib/output.js b/lib/output.js index 9ac50c08..fe982a7b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -592,7 +592,7 @@ function OutputStream(options) { || p instanceof AST_Call && p.expression === this; }); - PARENS(AST_Seq, function(output){ + PARENS(AST_Sequence, function(output){ var p = output.parent(); return p instanceof AST_Call // (foo, bar)() or foo(1, (2, 3), 4) || p instanceof AST_Unary // !(foo, bar, baz) @@ -1087,18 +1087,19 @@ function OutputStream(options) { AST_Call.prototype._codegen(self, output); }); - AST_Seq.DEFMETHOD("_do_print", function(output){ - this.car.print(output); - if (this.cdr) { - output.comma(); - if (output.should_break()) { - output.newline(); - output.indent(); + AST_Sequence.DEFMETHOD("_do_print", function(output){ + this.expressions.forEach(function(node, index) { + if (index > 0) { + output.comma(); + if (output.should_break()) { + output.newline(); + output.indent(); + } } - this.cdr.print(output); - } + node.print(output); + }); }); - DEFPRINT(AST_Seq, function(self, output){ + DEFPRINT(AST_Sequence, function(self, output){ self._do_print(output); // var p = output.parent(); // if (p instanceof AST_Statement) { diff --git a/lib/parse.js b/lib/parse.js index c34e13db..c7d75802 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1527,17 +1527,18 @@ function parse($TEXT, options) { var expression = function(commas, no_in) { var start = S.token; - var expr = maybe_assign(no_in); - if (commas && is("punc", ",")) { + var exprs = []; + while (true) { + exprs.push(maybe_assign(no_in)); + if (!commas || !is("punc", ",")) break; next(); - return new AST_Seq({ - start : start, - car : expr, - cdr : expression(true, no_in), - end : peek() - }); + commas = true; } - return expr; + return exprs.length == 1 ? exprs[0] : new AST_Sequence({ + start : start, + expressions : exprs, + end : peek() + }); }; function in_loop(cont) { diff --git a/lib/propmangle.js b/lib/propmangle.js index b6222990..aaf5936f 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -224,8 +224,8 @@ function mangle_properties(ast, options) { try { (function walk(node){ node.walk(new TreeWalker(function(node){ - if (node instanceof AST_Seq) { - walk(node.cdr); + if (node instanceof AST_Sequence) { + walk(node.expressions[node.expressions.length - 1]); return true; } if (node instanceof AST_String) { @@ -247,8 +247,9 @@ function mangle_properties(ast, options) { function mangleStrings(node) { return node.transform(new TreeTransformer(function(node){ - if (node instanceof AST_Seq) { - node.cdr = mangleStrings(node.cdr); + if (node instanceof AST_Sequence) { + var last = node.expressions.length - 1; + node.expressions[last] = mangleStrings(node.expressions[last]); } else if (node instanceof AST_String) { node.value = mangle(node.value); diff --git a/lib/transform.js b/lib/transform.js index 3018e8ff..112e5f28 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -174,9 +174,8 @@ TreeTransformer.prototype = new TreeWalker; self.args = do_list(self.args, tw); }); - _(AST_Seq, function(self, tw){ - self.car = self.car.transform(tw); - self.cdr = self.cdr.transform(tw); + _(AST_Sequence, function(self, tw){ + self.expressions = do_list(self.expressions, tw); }); _(AST_Dot, function(self, tw){ diff --git a/lib/utils.js b/lib/utils.js index fdb20471..e21fc5ec 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -346,7 +346,7 @@ function first_in_statement(stack) { for (var i = 0, p; p = stack.parent(i); i++) { if (p instanceof AST_Statement && p.body === node) return true; - if ((p instanceof AST_Seq && p.car === node ) || + if ((p instanceof AST_Sequence && p.expressions[0] === node) || (p instanceof AST_Call && p.expression === node && !(p instanceof AST_New) ) || (p instanceof AST_Dot && p.expression === node ) || (p instanceof AST_Sub && p.expression === node ) || diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 200b487f..7a6688ba 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -979,12 +979,12 @@ delete_conditional_1: { console.log(delete (1 ? 0 / 0 : x)); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((1 / 0, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((NaN, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } @@ -1006,12 +1006,12 @@ delete_conditional_2: { console.log(delete (0 ? x : 0 / 0)); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((Infinity, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((NaN, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } diff --git a/test/compress/evaluate.js b/test/compress/evaluate.js index 611acf0d..585ee2b9 100644 --- a/test/compress/evaluate.js +++ b/test/compress/evaluate.js @@ -922,12 +922,12 @@ delete_binary_1: { console.log(delete (true && (0 / 0))); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((1 / 0, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((NaN, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } @@ -948,12 +948,12 @@ delete_binary_2: { console.log(delete (false || (0 / 0))); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((Infinity, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((NaN, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } diff --git a/test/compress/issue-640.js b/test/compress/issue-640.js index fbf5f37f..c9a68dc9 100644 --- a/test/compress/issue-640.js +++ b/test/compress/issue-640.js @@ -159,7 +159,7 @@ negate_iife_4: { })(); } expect: { - (function(){ return t })() ? console.log(true) : console.log(false), function(){ + !function(){ return t }() ? console.log(false) : console.log(true), function(){ console.log("something"); }(); } @@ -183,7 +183,7 @@ negate_iife_5: { })(); } expect: { - (function(){ return t })() ? foo(true) : bar(false), function(){ + !function(){ return t }() ? bar(false) : foo(true), function(){ console.log("something"); }(); } @@ -207,7 +207,7 @@ negate_iife_5_off: { })(); } expect: { - (function(){ return t })() ? foo(true) : bar(false), function(){ + !function(){ return t }() ? bar(false) : foo(true), function(){ console.log("something"); }(); } diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 699341c0..3fb26278 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -460,7 +460,7 @@ issue_1758: { console.log(function(c) { var undefined = 42; return function() { - return c--, c--, c.toString(), void 0; + return c--, c--, void c.toString(); }(); }()); } @@ -481,12 +481,12 @@ delete_seq_1: { console.log(delete (1, 0 / 0)); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((1 / 0, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((0 / 0, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } @@ -505,12 +505,12 @@ delete_seq_2: { console.log(delete (1, 2, 0 / 0)); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((1 / 0, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((0 / 0, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } @@ -530,12 +530,12 @@ delete_seq_3: { console.log(delete (1, 2, 0 / 0)); } expect: { - console.log((void 0, !0)); - console.log((void 0, !0)); - console.log((Infinity, !0)); - console.log((1 / 0, !0)); - console.log((NaN, !0)); - console.log((0 / 0, !0)); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); + console.log(!0); } expect_stdout: true } @@ -606,7 +606,85 @@ delete_seq_6: { } expect: { var a; - console.log((a, !0)); + console.log(!0); } expect_stdout: true } + +side_effects: { + options = { + sequences: true, + side_effects: true, + } + input: { + 0, a(), 1, b(), 2, c(), 3; + } + expect: { + a(), b(), c(); + } +} + +side_effects_cascade_1: { + options = { + cascade: true, + conditionals: true, + sequences: true, + side_effects: true, + } + input: { + function f(a, b) { + a -= 42; + if (a < 0) a = 0; + b.a = a; + } + } + expect: { + function f(a, b) { + (a -= 42) < 0 && (a = 0), b.a = a; + } + } +} + +side_effects_cascade_2: { + options = { + cascade: true, + side_effects: true, + } + input: { + function f(a, b) { + b = a, + !a + (b += a) || (b += a), + b = a, + b; + } + } + expect: { + function f(a, b) { + b = a, + !a + (b += a) || (b += a), + b = a; + } + } +} + +side_effects_cascade_3: { + options = { + cascade: true, + conditionals: true, + side_effects: true, + } + input: { + function f(a, b) { + "foo" ^ (b += a), + b ? false : (b = a) ? -1 : (b -= a) - (b ^= a), + a-- || !a, + a; + } + } + expect: { + function f(a, b) { + !(b += a) && ((b = a) || (b -= a, b ^= a)), + --a; + } + } +} From 32deb365d500f22f3215fe0edb094b38dba5b61a Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 15 Apr 2017 05:52:29 +0800 Subject: [PATCH 02/36] drop `angular` (#1812) Remove support for `@ngInject` as there are proper alternatives anyway. --- lib/compress.js | 78 --------------------------------- test/compress/angular-inject.js | 67 ---------------------------- 2 files changed, 145 deletions(-) delete mode 100644 test/compress/angular-inject.js diff --git a/lib/compress.js b/lib/compress.js index 8c254573..5128abb2 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -48,7 +48,6 @@ function Compressor(options, false_by_default) { return new Compressor(options, false_by_default); TreeTransformer.call(this, this.before, this.after); this.options = defaults(options, { - angular : false, booleans : !false_by_default, cascade : !false_by_default, collapse_vars : !false_by_default, @@ -536,9 +535,6 @@ merge(Compressor.prototype, { var CHANGED, max_iter = 10; do { CHANGED = false; - if (compressor.option("angular")) { - statements = process_for_angular(statements); - } statements = eliminate_spurious_blocks(statements); if (compressor.option("dead_code")) { statements = eliminate_dead_code(statements, compressor); @@ -731,80 +727,6 @@ merge(Compressor.prototype, { } } - function process_for_angular(statements) { - function has_inject(comment) { - return /@ngInject/.test(comment.value); - } - function make_arguments_names_list(func) { - return func.argnames.map(function(sym){ - return make_node(AST_String, sym, { value: sym.name }); - }); - } - function make_array(orig, elements) { - return make_node(AST_Array, orig, { elements: elements }); - } - function make_injector(func, name) { - return make_node(AST_SimpleStatement, func, { - body: make_node(AST_Assign, func, { - operator: "=", - left: make_node(AST_Dot, name, { - expression: make_node(AST_SymbolRef, name, name), - property: "$inject" - }), - right: make_array(func, make_arguments_names_list(func)) - }) - }); - } - function check_expression(body) { - if (body && body.args) { - // if this is a function call check all of arguments passed - body.args.forEach(function(argument, index, array) { - var comments = argument.start.comments_before; - // if the argument is function preceded by @ngInject - if (argument instanceof AST_Lambda && comments.length && has_inject(comments[0])) { - // replace the function with an array of names of its parameters and function at the end - array[index] = make_array(argument, make_arguments_names_list(argument).concat(argument)); - } - }); - // if this is chained call check previous one recursively - if (body.expression && body.expression.expression) { - check_expression(body.expression.expression); - } - } - } - return statements.reduce(function(a, stat){ - a.push(stat); - - if (stat.body && stat.body.args) { - check_expression(stat.body); - } else { - var token = stat.start; - var comments = token.comments_before; - if (comments && comments.length > 0) { - var last = comments.pop(); - if (has_inject(last)) { - // case 1: defun - if (stat instanceof AST_Defun) { - a.push(make_injector(stat, stat.name)); - } - else if (stat instanceof AST_Definitions) { - stat.definitions.forEach(function(def) { - if (def.value && def.value instanceof AST_Lambda) { - a.push(make_injector(def.value, def.name)); - } - }); - } - else { - compressor.warn("Unknown statement marked with @ngInject [{file}:{line},{col}]", token); - } - } - } - } - - return a; - }, []); - } - function eliminate_spurious_blocks(statements) { var seen_dirs = []; return statements.reduce(function(a, stat){ diff --git a/test/compress/angular-inject.js b/test/compress/angular-inject.js deleted file mode 100644 index 8b24c846..00000000 --- a/test/compress/angular-inject.js +++ /dev/null @@ -1,67 +0,0 @@ -ng_inject_defun: { - options = { - angular: true - }; - input: { - /*@ngInject*/ - function Controller(dependency) { - return dependency; - } - } - expect: { - function Controller(dependency) { - return dependency; - } - Controller.$inject=['dependency'] - } -} - -ng_inject_assignment: { - options = { - angular: true - }; - input: { - /*@ngInject*/ - var Controller = function(dependency) { - return dependency; - } - } - expect: { - var Controller = function(dependency) { - return dependency; - } - Controller.$inject=['dependency'] - } -} - -ng_inject_inline: { - options = { - angular: true - }; - input: { - angular.module('a'). - factory('b', - /*@ngInject*/ - function(dependency) { - return dependency; - }). - directive('c', - /*@ngInject*/ - function(anotherDependency) { - return anotherDependency; - }) - } - expect: { - angular.module('a'). - factory('b',[ - 'dependency', - function(dependency) { - return dependency; - }]). - directive('c',[ - 'anotherDependency', - function(anotherDependency) { - return anotherDependency; - }]) - } -} From ec443e422c220619fe671166e467d3d8838b0cab Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 15 Apr 2017 23:50:50 +0800 Subject: [PATCH 03/36] unify CLI & API under `minify()` (#1811) - rename `screw_ie8` to `ie8` - rename `mangle.except` to `mangle.reserved` - rename `mangle.properties.ignore_quoted` to `mangle.properties.keep_quoted` - compact `sourceMap` options - more stringent verification on input `options` - toplevel shorthands - `ie8` - `keep_fnames` - `toplevel` - `warnings` - support arrays and unquoted string values on CLI - drop `fromString` from `minify()` - `minify()` no longer handles any `fs` operations - unify order of operations for `mangle_properties()` on CLI & API - `bin/uglifyjs` used to `mangle_properties()` before even `Compressor` - `minify()` used to `mangle_properties()` after `Compressor` but before `mangle_names()` - both will now do `Compressor`, `mangle_names()` then `mangle_properties()` - `options.parse` / `--parse` for parser options beyond `bare_returns` - add `mangle.properties.builtins` to disable built-in reserved list - disable with `--mangle-props builtins` on CLI - `warnings` now off by default - add `--warn` and `--verbose` on CLI - drop `--enclose` - drop `--export-all` - drop `--reserved-file` - use `--mangle reserved` instead - drop `--reserve-domprops` - enabled by default, disable with `--mangle-props domprops` - drop `--prefix` - use `--source-map base` instead - drop `--lint` - remove `bin/extract-props.js` - limit exposure of internal APIs - update documentations closes #96 closes #102 closes #136 closes #166 closes #243 closes #254 closes #261 closes #311 closes #700 closes #748 closes #912 closes #1072 closes #1366 fixes #101 fixes #123 fixes #124 fixes #263 fixes #379 fixes #419 fixes #423 fixes #461 fixes #465 fixes #576 fixes #737 fixes #772 fixes #958 fixes #1036 fixes #1142 fixes #1175 fixes #1220 fixes #1223 fixes #1280 fixes #1359 fixes #1368 --- README.md | 519 +- bin/extract-props.js | 77 - bin/uglifyjs | 894 +- lib/ast.js | 59 +- lib/compress.js | 27 +- lib/minify.js | 146 + lib/output.js | 10 +- lib/parse.js | 2 - lib/propmangle.js | 52 +- lib/scope.js | 109 +- package.json | 4 +- test/benchmark.js | 10 +- test/compress/ascii.js | 5 +- test/compress/issue-1321.js | 6 +- test/compress/issue-1446.js | 2 +- test/compress/issue-1588.js | 8 +- test/compress/issue-1704.js | 64 +- test/compress/issue-1733.js | 8 +- test/compress/loops.js | 16 +- test/compress/properties.js | 12 +- test/compress/screw-ie8.js | 72 +- test/jetstream.js | 2 +- test/mocha/cli.js | 89 +- test/mocha/comment-filter.js | 2 - test/mocha/comment.js | 4 +- test/mocha/comment_before_constant.js | 5 - test/mocha/directives.js | 10 +- test/mocha/glob.js | 112 +- test/mocha/huge-number-of-comments.js | 7 +- test/mocha/input-sourcemaps.js | 6 +- test/mocha/let.js | 2 +- test/mocha/line-endings.js | 3 +- test/mocha/minify-file-map.js | 30 +- test/mocha/minify.js | 80 +- test/mocha/new.js | 2 - test/mocha/release.js | 4 +- test/mocha/screw-ie8.js | 4 +- test/mocha/spidermonkey.js | 8 +- test/ufuzz.js | 10 +- test/ufuzz.json | 15 +- tools/domprops.json | 11204 ++++++++++++------------ tools/exports.js | 6 +- tools/node.js | 261 +- 43 files changed, 6566 insertions(+), 7402 deletions(-) delete mode 100755 bin/extract-props.js create mode 100644 lib/minify.js diff --git a/README.md b/README.md index d15f114c..42d2e87d 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,6 @@ UglifyJS 2 UglifyJS is a JavaScript parser, minifier, compressor or beautifier toolkit. -This page documents the command line utility. For -[API and internals documentation see my website](http://lisperator.net/uglifyjs/). -There's also an -[in-browser online demo](http://lisperator.net/uglifyjs/#demo) (for Firefox, -Chrome and probably Safari). - #### Note: - release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify ES2015+ (ES6+) code then please use the [harmony](#harmony) development branch. @@ -57,48 +51,52 @@ a double dash to prevent input files being used as option arguments: The available options are: ``` - --source-map Specify an output file where to generate source - map. - --source-map-root The path to the original source to be included - in the source map. - --source-map-url The path to the source map to be added in //# - sourceMappingURL. Defaults to the value passed - with --source-map. - --source-map-include-sources Pass this flag if you want to include the - content of source files in the source map as - sourcesContent property. - --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. 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. - --support-ie8 Use this flag to support Internet Explorer 6/7/8. - Equivalent to setting `screw_ie8: false` in `minify()` - for `compress`, `mangle` and `output` options. - --expr Parse a single expression, rather than a - program (for parsing JSON) - -p, --prefix Skip prefix for original filenames that appear - in source maps. For example -p 3 will drop 3 - directories from file names and ensure they are - relative paths. You can also specify -p - relative, which will make UglifyJS figure out - itself the relative paths between original - sources, the source map and the output file. - -o, --output Output file (default STDOUT). - -b, --beautify Beautify output/specify output options. - -m, --mangle Mangle names/pass mangler options. - -r, --reserved Reserved names to exclude from mangling. - -c, --compress Enable compressor/pass compressor options, e.g. - `-c 'if_return=false,pure_funcs=["Math.pow","console.log"]'` - Use `-c` with no argument to enable default compression - options. - -d, --define Global definitions - -e, --enclose Embed everything in a big function, with a - configurable parameter/argument list. - --comments Preserve copyright comments in the output. By + -h, --help Print usage information. + -V, --version Print version number. + -p, --parse Specify parser options: + `acorn` Use Acorn for parsing. + `bare_returns` Allow return outside of functions. + Useful when minifying CommonJS + modules and Userscripts that may + be anonymous function wrapped (IIFE) + by the .user.js engine `caller`. + `expression` Parse a single expression, rather than + a program (for parsing JSON). + `spidermonkey` Assume input files are SpiderMonkey + AST format (as JSON). + -c, --compress [options] Enable compressor/specify compressor options: + `pure_funcs` List of functions that can be safely + removed when their return values are + not used. + -m, --mangle [options] Mangle names/specify mangler options: + `reserved` List of names that should not be mangled. + --mangle-props [options] Mangle properties/specify mangler options: + `builtins` Mangle property names that overlaps + with standard JavaScript globals. + `debug` Add debug prefix and suffix. + `domprops` Mangle property names that overlaps + with DOM properties. + `keep_quoted` Only mangle unquoted properies. + `regex` Only mangle matched property names. + `reserved` List of names that should not be mangled. + -b, --beautify [options] Beautify output/specify output options: + `beautify` Enabled with `--beautify` by default. + `preamble` Preamble to prepend to the output. You + can use this to insert a comment, for + example for licensing information. + This will not be parsed, but the source + map will adjust for its presence. + `quote_style` Quote style: + 0 - auto + 1 - single + 2 - double + 3 - original + `wrap_iife` Wrap IIFEs in parenthesis. Note: you may + want to disable `negate_iife` under + compressor options. + -o, --output Output file (default STDOUT). Specify "spidermonkey" + to dump SpiderMonkey AST format (as JSON) to STDOUT. + --comments [filter] Preserve copyright comments in the output. By default this works like Google Closure, keeping JSDoc-style comments that contain "@license" or "@preserve". You can optionally pass one of the @@ -110,54 +108,39 @@ The available options are: kept when compression is on, because of dead code removal or cascading statements into sequences. - --preamble Preamble to prepend to the output. You can use - this to insert a comment, for example for - licensing information. This will not be - parsed, but the source map will adjust for its - presence. - --stats Display operations run time on STDERR. - --acorn Use Acorn for parsing. - --spidermonkey Assume input files are SpiderMonkey AST format - (as JSON). - --self Build itself (UglifyJS2) as a library (implies - --wrap=UglifyJS --export-all) - --wrap Embed everything in a big function, making the + --config-file Read `minify()` options from JSON file. + -d, --define [=value] Global definitions. + --ie8 Support non-standard Internet Explorer 8. + Equivalent to setting `ie8: true` in `minify()` + for `compress`, `mangle` and `output` options. + By default UglifyJS will not try to be IE-proof. + --keep-fnames Do not mangle/drop function names. Useful for + code relying on Function.prototype.name. + --name-cache File to hold mangled name mappings. + --self Build UglifyJS2 as a library (implies --wrap UglifyJS) + --source-map [options] Enable source map/specify source map options: + `base` Path to compute relative paths from input files. + `content` Input source map, useful if you're compressing + JS that was generated from some other original + code. Specify "inline" if the source map is + included within the sources. + `filename` Name and/or location of the output source. + `includeSources` Pass this flag if you want to include + the content of source files in the + source map as sourcesContent property. + `root` Path to the original source to be included in + the source map. + `url` If specified, path to the source map to append in + `//# sourceMappingURL`. + --stats Display operations run time on STDERR. + --toplevel Compress and/or mangle variables in toplevel scope. + --verbose Print diagnostic messages. + --warn Print warning messages. + --wrap Embed everything in a big function, making the “exports” and “global” variables available. You need to pass an argument to this option to specify the name that your module will take when included in, say, a browser. - --export-all Only used when --wrap, this tells UglifyJS to - add code to automatically export all globals. - --lint Display some scope warnings - -v, --verbose Verbose - -V, --version Print version number and exit. - --noerr Don't throw an error for unknown options in -c, - -b or -m. - --bare-returns Allow return outside of functions. Useful when - minifying CommonJS modules and Userscripts that - may be anonymous function wrapped (IIFE) by the - .user.js engine `caller`. - --keep-fnames Do not mangle/drop function names. Useful for - code relying on Function.prototype.name. - --reserved-file File containing reserved names - --reserve-domprops Make (most?) DOM properties reserved for - --mangle-props - --mangle-props Mangle property names (default `0`). Set to - `true` or `1` to mangle all property names. Set - to `unquoted` or `2` to only mangle unquoted - property names. Mode `2` also enables the - `keep_quoted_props` beautifier option to - preserve the quotes around property names and - disables the `properties` compressor option to - prevent rewriting quoted properties with dot - notation. You can override these by setting - them explicitly on the command line. - --mangle-regex Only mangle property names matching the regex - --name-cache File to hold mangled names mappings - --pure-funcs Functions that can be safely removed if their - return value is not used, e.g. - `--pure-funcs Math.floor console.info` - (requires `--compress`) ``` Specify `--output` (`-o`) to declare the output file. Otherwise the output @@ -167,23 +150,19 @@ goes to STDOUT. UglifyJS2 can generate a source map file, which is highly useful for debugging your compressed JavaScript. To get a source map, pass -`--source-map output.js.map` (full path to the file where you want the -source map dumped). +`--source-map --output output.js` (source map will be written out to +`output.js.map`). -Additionally you might need `--source-map-root` to pass the URL where the -original files can be found. In case you are passing full paths to input -files to UglifyJS, you can use `--prefix` (`-p`) to specify the number of -directories to drop from the path prefix when declaring files in the source -map. +Additionally you might need `--source-map root=` to pass the URL where +the original files can be found. Use `--source-map url=` to specify +the URL where the source map can be found. For example: uglifyjs /home/doe/work/foo/src/js/file1.js \ /home/doe/work/foo/src/js/file2.js \ - -o foo.min.js \ - --source-map foo.min.js.map \ - --source-map-root http://foo.com/src \ - -p 5 -c -m + -o foo.min.js -c -m \ + --source-map base="/home/doe/work/foo/src",root="http://foo.com/src" The above will compress and mangle `file1.js` and `file2.js`, will drop the output in `foo.min.js` and the source map in `foo.min.js.map`. The source @@ -220,10 +199,10 @@ To enable the mangler you need to pass `--mangle` (`-m`). The following (disabled by default). When mangling is enabled but you want to prevent certain names from being -mangled, you can declare those names with `--reserved` (`-r`) — pass a +mangled, you can declare those names with `--mangle reserved` — pass a comma-separated list of names. For example: - uglifyjs ... -m -r '$,require,exports' + uglifyjs ... -m reserved=[$,require,exports] to prevent the `require`, `exports` and `$` names from being changed. @@ -248,39 +227,22 @@ console.log(x.something()); In the above code, `foo`, `bar`, `baz`, `moo` and `boo` will be replaced with single characters, while `something()` will be left as is. -In order for this to be of any use, we should avoid mangling standard JS -names. For instance, if your code would contain `x.length = 10`, then -`length` becomes a candidate for mangling and it will be mangled throughout -the code, regardless if it's being used as part of your own objects or -accessing an array's length. To avoid that, you can use `--reserved-file` -to pass a filename that should contain the names to be excluded from -mangling. This file can be used both for excluding variable names and -property names. It could look like this, for example: - -```js -{ - "vars": [ "define", "require", ... ], - "props": [ "length", "prototype", ... ] -} -``` - -`--reserved-file` can be an array of file names (either a single -comma-separated argument, or you can pass multiple `--reserved-file` -arguments) — in this case it will exclude names from all those files. +In order for this to be of any use, we avoid mangling standard JS names by +default (`--mangle-props builtins` to override). A default exclusion file is provided in `tools/domprops.json` which should cover most standard JS and DOM properties defined in various browsers. Pass -`--reserve-domprops` to read that in. +`--mangle-props domprops` to disable this feature. You can also use a regular expression to define which property names should be -mangled. For example, `--mangle-regex="/^_/"` will only mangle property names -that start with an underscore. +mangled. For example, `--mangle-props regex=/^_/` will only mangle property +names that start with an underscore. When you compress multiple files using this option, in order for them to work together in the end we need to ensure somehow that one property gets -mangled to the same name in all of them. For this, pass `--name-cache -filename.json` and UglifyJS will maintain these mappings in a file which can -then be reused. It should be initially empty. Example: +mangled to the same name in all of them. For this, pass `--name-cache filename.json` +and UglifyJS will maintain these mappings in a file which can then be reused. +It should be initially empty. Example: ``` rm -f /tmp/cache.json # start fresh @@ -294,26 +256,26 @@ of mangled property names. Using the name cache is not necessary if you compress all your files in a single call to UglifyJS. -#### Mangling unquoted names (`--mangle-props=unquoted` or `--mangle-props=2`) +#### Mangling unquoted names (`--mangle-props keep_quoted`) Using quoted property name (`o["foo"]`) reserves the property name (`foo`) so that it is not mangled throughout the entire script even when used in an unquoted style (`o.foo`). Example: ``` -$ echo 'var o={"foo":1, bar:3}; o.foo += o.bar; console.log(o.foo);' | uglifyjs --mangle-props=2 -mc -var o={"foo":1,a:3};o.foo+=o.a,console.log(o.foo); +$ echo 'var o={"foo":1, bar:3}; o.foo += o.bar; console.log(o.foo);' | uglifyjs --mangle-props keep_quoted -mc +var o={foo:1,a:3};o.foo+=o.a,console.log(o.foo); ``` #### Debugging property name mangling -You can also pass `--mangle-props-debug` in order to mangle property names +You can also pass `--mangle-props debug` in order to mangle property names without completely obscuring them. For example the property `o.foo` would mangle to `o._$foo$_` with this option. This allows property mangling of a large codebase while still being able to debug the code and identify where mangling is breaking things. -You can also pass a custom suffix using `--mangle-props-debug=XYZ`. This would then +You can also pass a custom suffix using `--mangle-props debug=XYZ`. This would then mangle `o.foo` to `o._$foo$XYZ_`. You can change this each time you compile a script to identify how a property got mangled. One technique is to pass a random number on every compile to simulate mangling changing with different @@ -501,8 +463,6 @@ code as usual. The build will contain the `const` declarations if you use them. If you are targeting < ES6 environments which does not support `const`, using `var` with `reduce_vars` (enabled by default) should suffice. - - #### Conditional compilation, API You can also use conditional compilation via the programmatic API. With the difference that the property name is `global_defs` and is a compressor property: @@ -570,8 +530,8 @@ You can pass `--comments` to retain certain comments in the output. By default it will keep JSDoc-style comments that contain "@preserve", "@license" or "@cc_on" (conditional compilation for IE). You can pass `--comments all` to keep all the comments, or a valid JavaScript regexp to -keep only comments that match this regexp. For example `--comments -'/foo|bar/'` will keep only comments that contain "foo" or "bar". +keep only comments that match this regexp. For example `--comments /^!/` +will keep comments like `/*! Copyright Notice */`. Note, however, that there might be situations where comments are lost. For example: @@ -604,16 +564,16 @@ SpiderMonkey AST. It has a small CLI utility that parses one file and dumps the AST in JSON on the standard output. To use UglifyJS to mangle and compress that: - acorn file.js | uglifyjs --spidermonkey -m -c + acorn file.js | uglifyjs -p spidermonkey -m -c -The `--spidermonkey` option tells UglifyJS that all input files are not +The `-p spidermonkey` option tells UglifyJS that all input files are not JavaScript, but JS code described in SpiderMonkey AST in JSON. Therefore we don't use our own parser in this case, but just transform that AST into our internal AST. ### Use Acorn for parsing -More for fun, I added the `--acorn` option which will use Acorn to do all +More for fun, I added the `-p acorn` option which will use Acorn to do all the parsing. If you pass this option, UglifyJS will `require("acorn")`. Acorn is really fast (e.g. 250ms instead of 380ms on some 650K code), but @@ -661,107 +621,71 @@ like this: var UglifyJS = require("uglify-js"); ``` -It exports a lot of names, but I'll discuss here the basics that are needed -for parsing, mangling and compressing a piece of code. The sequence is (1) -parse, (2) compress, (3) mangle, (4) generate output code. - -### The simple way - -There's a single toplevel function which combines all the steps. If you -don't need additional customization, you might want to go with `minify`. +There is a single toplevel function, `minify(files, options)`, which will +performs all the steps in a configurable manner. Example: ```javascript -var result = UglifyJS.minify("/path/to/file.js"); +var result = UglifyJS.minify("var b = function() {};"); console.log(result.code); // minified output -// if you need to pass code instead of file name -var result = UglifyJS.minify("var b = function () {};", {fromString: true}); ``` You can also compress multiple files: ```javascript -var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ]); +var result = UglifyJS.minify({ + "file1.js": "var a = function() {};", + "file2.js": "var b = function() {};" +}); console.log(result.code); ``` To generate a source map: ```javascript -var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { - outSourceMap: "out.js.map" +var result = UglifyJS.minify({"file1.js": "var a = function() {};"}, { + sourceMap: { + filename: "out.js", + url: "out.js.map" + } }); console.log(result.code); // minified output -console.log(result.map); -``` - -To generate a source map with the fromString option, you can also use an object: -```javascript -var result = UglifyJS.minify({"file1.js": "var a = function () {};"}, { - outSourceMap: "out.js.map", - outFileName: "out.js", - fromString: true -}); +console.log(result.map); // source map ``` Note that the source map is not saved in a file, it's just returned in -`result.map`. The value passed for `outSourceMap` is only used to set +`result.map`. The value passed for `sourceMap.url` is only used to set `//# sourceMappingURL=out.js.map` in `result.code`. The value of -`outFileName` is only used to set `file` attribute in source map file. +`filename` is only used to set `file` attribute (see [the spec][sm-spec]) +in source map file. -The `file` attribute in the source map (see [the spec][sm-spec]) will -use `outFileName` firstly, if it's falsy, then will be deduced from -`outSourceMap` (by removing `'.map'`). - -You can set option `sourceMapInline` to be `true` and source map will +You can set option `sourceMap.url` to be `"inline"` and source map will be appended to code. You can also specify sourceRoot property to be included in source map: ```javascript -var result = UglifyJS.minify([ "file1.js", "file2.js", "file3.js" ], { - outSourceMap: "out.js.map", - sourceRoot: "http://example.com/src" +var result = UglifyJS.minify({"file1.js": "var a = function() {};"}, { + sourceMap: { + root: "http://example.com/src", + url: "out.js.map" }); ``` If you're compressing compiled JavaScript and have a source map for it, you -can use the `inSourceMap` argument: +can use `sourceMap.content`: ```javascript -var result = UglifyJS.minify("compiled.js", { - inSourceMap: "compiled.js.map", - outSourceMap: "minified.js.map" +var result = UglifyJS.minify({"compiled.js": "compiled code"}, { + sourceMap: { + content: "content from compiled.js.map", + url: "minified.js.map" + } }); // same as before, it returns `code` and `map` ``` -If your input source map is not in a file, you can pass it in as an object -using the `inSourceMap` argument: - -```javascript -var result = UglifyJS.minify("compiled.js", { - inSourceMap: JSON.parse(my_source_map_string), - outSourceMap: "minified.js.map" -}); -``` - -The `inSourceMap` is only used if you also request `outSourceMap` (it makes -no sense otherwise). - -To set the source map url, use the `sourceMapUrl` option. -If you're using the X-SourceMap header instead, you can just set the `sourceMapUrl` option to false. -Defaults to outSourceMap: - -```javascript -var result = UglifyJS.minify([ "file1.js" ], { - outSourceMap: "out.js.map", - sourceMapUrl: "localhost/out.js.map" -}); -``` +If you're using the `X-SourceMap` header instead, you can just omit `sourceMap.url`. Other options: - `warnings` (default `false`) — pass `true` to display compressor warnings. -- `fromString` (default `false`) — if you pass `true` then you can pass - JavaScript source code, rather than file names. - - `mangle` (default `true`) — pass `false` to skip mangling names, or pass an object to specify mangling options (see below). @@ -769,18 +693,18 @@ Other options: mangle property options. - `output` (default `null`) — pass an object if you wish to specify - additional [output options][codegen]. The defaults are optimized + additional [output options](#beautifier-options). The defaults are optimized for best compression. - `compress` (default `{}`) — pass `false` to skip compressing entirely. - Pass an object to specify custom [compressor options][compressor]. + Pass an object to specify custom [compressor options](#compressor-options). - `parse` (default {}) — pass an object if you wish to specify some - additional [parser options][parser]. (not all options available... see below) + additional [parser options](#the-parser). ##### mangle - - `except` - pass an array of identifiers that should be excluded from mangling + - `reserved` - pass an array of identifiers that should be excluded from mangling - `toplevel` — mangle names declared in the toplevel scope (disabled by default). @@ -805,183 +729,22 @@ Other options: UglifyJS.minify("tst.js").code; // 'function funcName(a,n){}var globalVar;' - UglifyJS.minify("tst.js", { mangle: { except: ['firstLongName'] } }).code; + UglifyJS.minify("tst.js", { mangle: { reserved: ['firstLongName'] } }).code; // 'function funcName(firstLongName,a){}var globalVar;' UglifyJS.minify("tst.js", { mangle: { toplevel: true } }).code; // 'function n(n,a){}var a;' ``` -##### mangleProperties options +##### mangle.properties options - - `regex` — Pass a RegExp to only mangle certain names (maps to the `--mangle-regex` CLI arguments option) - - `ignore_quoted` – Only mangle unquoted property names (maps to the `--mangle-props 2` CLI arguments option) - - `debug` – Mangle names with the original name still present (maps to the `--mangle-props-debug` CLI arguments option). Defaults to `false`. Pass an empty string to enable, or a non-empty string to set the suffix. - -We could add more options to `UglifyJS.minify` — if you need additional -functionality please suggest! - -### The hard way - -Following there's more detailed API info, in case the `minify` function is -too simple for your needs. - -#### The parser -```javascript -var toplevel_ast = UglifyJS.parse(code, options); -``` - -`options` is optional and if present it must be an object. The following -properties are available: - -- `strict` — disable automatic semicolon insertion and support for trailing - comma in arrays and objects -- `bare_returns` — Allow return outside of functions. (maps to the - `--bare-returns` CLI arguments option and available to `minify` `parse` - other options object) -- `filename` — the name of the file where this code is coming from -- `toplevel` — a `toplevel` node (as returned by a previous invocation of - `parse`) - -The last two options are useful when you'd like to minify multiple files and -get a single file as the output and a proper source map. Our CLI tool does -something like this: -```javascript -var toplevel = null; -files.forEach(function(file){ - var code = fs.readFileSync(file, "utf8"); - toplevel = UglifyJS.parse(code, { - filename: file, - toplevel: toplevel - }); -}); -``` - -After this, we have in `toplevel` a big AST containing all our files, with -each token having proper information about where it came from. - -#### Scope information - -UglifyJS contains a scope analyzer that you need to call manually before -compressing or mangling. Basically it augments various nodes in the AST -with information about where is a name defined, how many times is a name -referenced, if it is a global or not, if a function is using `eval` or the -`with` statement etc. I will discuss this some place else, for now what's -important to know is that you need to call the following before doing -anything with the tree: -```javascript -toplevel.figure_out_scope() -``` - -#### Compression - -Like this: -```javascript -var compressor = UglifyJS.Compressor(options); -var compressed_ast = compressor.compress(toplevel); -``` - -The `options` can be missing. Available options are discussed above in -“Compressor options”. Defaults should lead to best compression in most -scripts. - -The compressor is destructive, so don't rely that `toplevel` remains the -original tree. - -#### Mangling - -After compression it is a good idea to call again `figure_out_scope` (since -the compressor might drop unused variables / unreachable code and this might -change the number of identifiers or their position). Optionally, you can -call a trick that helps after Gzip (counting character frequency in -non-mangleable words). Example: -```javascript -compressed_ast.figure_out_scope(); -compressed_ast.compute_char_frequency(); -compressed_ast.mangle_names(); -``` - -#### Generating output - -AST nodes have a `print` method that takes an output stream. Essentially, -to generate code you do this: -```javascript -var stream = UglifyJS.OutputStream(options); -compressed_ast.print(stream); -var code = stream.toString(); // this is your minified code -``` - -or, for a shortcut you can do: -```javascript -var code = compressed_ast.print_to_string(options); -``` - -As usual, `options` is optional. The output stream accepts a lot of options, -most of them documented above in section “Beautifier options”. The two -which we care about here are `source_map` and `comments`. - -#### Keeping comments in the output - -In order to keep certain comments in the output you need to pass the -`comments` option. Pass a RegExp (as string starting and closing with `/` -or pass a RegExp object), a boolean or a function. Stringified options -`all` and `some` can be passed too, where `some` behaves like it's cli -equivalent `--comments` without passing a value. If you pass a RegExp, -only those comments whose body matches the RegExp will be kept. Note that body -means without the initial `//` or `/*`. If you pass a function, it will be -called for every comment in the tree and will receive two arguments: the -node that the comment is attached to, and the comment token itself. - -The comment token has these properties: - -- `type`: "comment1" for single-line comments or "comment2" for multi-line - comments -- `value`: the comment body -- `pos` and `endpos`: the start/end positions (zero-based indexes) in the - original code where this comment appears -- `line` and `col`: the line and column where this comment appears in the - original code -- `file` — the file name of the original file -- `nlb` — true if there was a newline before this comment in the original - code, or if this comment contains a newline. - -Your function should return `true` to keep the comment, or a falsy value -otherwise. - -#### Generating a source mapping - -You need to pass the `source_map` argument when calling `print`. It needs -to be a `SourceMap` object (which is a thin wrapper on top of the -[source-map][source-map] library). - -Example: -```javascript -var source_map = UglifyJS.SourceMap(source_map_options); -var stream = UglifyJS.OutputStream({ - ... - source_map: source_map -}); -compressed_ast.print(stream); - -var code = stream.toString(); -var map = source_map.toString(); // json output for your source map -``` - -The `source_map_options` (optional) can contain the following properties: - -- `file`: the name of the JavaScript output file that this mapping refers to -- `root`: the `sourceRoot` property (see the [spec][sm-spec]) -- `orig`: the "original source map", handy when you compress generated JS - and want to map the minified output back to the original code where it - came from. It can be simply a string in JSON, or a JSON object containing - the original source map. + - `regex` — Pass a RegExp to only mangle certain names + - `keep_quoted` — Only mangle unquoted property names + - `debug` — Mangle names with the original name still present. Defaults to `false`. + Pass an empty string to enable, or a non-empty string to set the suffix. [acorn]: https://github.com/ternjs/acorn - [source-map]: https://github.com/mozilla/source-map - [sm-spec]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit - [codegen]: http://lisperator.net/uglifyjs/codegen - [compressor]: http://lisperator.net/uglifyjs/compress - [parser]: http://lisperator.net/uglifyjs/parser + [sm-spec]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k #### Harmony diff --git a/bin/extract-props.js b/bin/extract-props.js deleted file mode 100755 index 7ce7d31e..00000000 --- a/bin/extract-props.js +++ /dev/null @@ -1,77 +0,0 @@ -#! /usr/bin/env node - -"use strict"; - -var U2 = require("../tools/node"); -var fs = require("fs"); -var yargs = require("yargs"); -var ARGS = yargs - .describe("o", "Output file") - .argv; -var files = ARGS._.slice(); -var output = { - vars: {}, - props: {} -}; - -if (ARGS.o) try { - output = JSON.parse(fs.readFileSync(ARGS.o, "utf8")); -} catch(ex) {} - -files.forEach(getProps); - -if (ARGS.o) { - fs.writeFileSync(ARGS.o, JSON.stringify(output, null, 2), "utf8"); -} else { - console.log("%s", JSON.stringify(output, null, 2)); -} - -function getProps(filename) { - var code = fs.readFileSync(filename, "utf8"); - var ast = U2.parse(code); - - ast.walk(new U2.TreeWalker(function(node){ - if (node instanceof U2.AST_ObjectKeyVal) { - add(node.key); - } - else if (node instanceof U2.AST_ObjectProperty) { - add(node.key.name); - } - else if (node instanceof U2.AST_Dot) { - add(node.property); - } - else if (node instanceof U2.AST_Sub) { - addStrings(node.property); - } - })); - - function addStrings(node) { - var out = {}; - try { - (function walk(node){ - node.walk(new U2.TreeWalker(function(node){ - if (node instanceof U2.AST_Sequence) { - walk(node.expressions[node.expressions.length - 1]); - return true; - } - if (node instanceof U2.AST_String) { - add(node.value); - return true; - } - if (node instanceof U2.AST_Conditional) { - walk(node.consequent); - walk(node.alternative); - return true; - } - throw out; - })); - })(node); - } catch(ex) { - if (ex !== out) throw ex; - } - } - - function add(name) { - output.props[name] = true; - } -} diff --git a/bin/uglifyjs b/bin/uglifyjs index ef776492..96eef27e 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -3,633 +3,333 @@ "use strict"; -var UglifyJS = require("../tools/node"); -var sys = require("util"); -var yargs = require("yargs"); -var fs = require("fs"); -var path = require("path"); -var acorn; -var screw_ie8 = true; -var ARGS = yargs - .usage("$0 input1.js [input2.js ...] [options]\n\ -Use a single dash to read input from the standard input.\ -\n\n\ -NOTE: by default there is no mangling/compression.\n\ -Without [options] it will simply parse input files and dump the AST\n\ -with whitespace and comments discarded. To achieve compression and\n\ -mangling you need to use `-c` and `-m`.\ -") - .describe("source-map", "Specify an output file where to generate source map.") - .describe("source-map-root", "The path to the original source to be included in the source map.") - .describe("source-map-url", "The path to the source map to be added in //# sourceMappingURL. Defaults to the value passed with --source-map.") - .describe("source-map-inline", "Write base64-encoded source map to the end of js output. Disabled by default") - .describe("source-map-include-sources", "Pass this flag if you want to include the content of source files in the source map as sourcesContent property.") - .describe("in-source-map", "Input source map, useful if you're compressing JS that was generated from some other original code.") - .describe("screw-ie8", "Do not support Internet Explorer 6/7/8. This flag is enabled by default.") - .describe("support-ie8", "Support non-standard Internet Explorer 6/7/8 javascript.") - .describe("expr", "Parse a single expression, rather than a program (for parsing JSON)") - .describe("p", "Skip prefix for original filenames that appear in source maps. \ -For example -p 3 will drop 3 directories from file names and ensure they are relative paths. \ -You can also specify -p relative, which will make UglifyJS figure out itself the relative paths between original sources, \ -the source map and the output file.") - .describe("o", "Output file (default STDOUT).") - .describe("b", "Beautify output/specify output options.") - .describe("m", "Mangle names/pass mangler options.") - .describe("r", "Reserved names to exclude from mangling.") - .describe("c", "Enable compressor/pass compressor options. \ -Pass options like -c hoist_vars=false,if_return=false. \ -Use -c with no argument to use the default compression options.") - .describe("d", "Global definitions") - .describe("e", "Embed everything in a big function, with a configurable parameter/argument list.") - - .describe("comments", "Preserve copyright comments in the output. \ -By default this works like Google Closure, keeping JSDoc-style comments that contain \"@license\" or \"@preserve\". \ -You can optionally pass one of the following arguments to this flag:\n\ -- \"all\" to keep all comments\n\ -- a valid JS RegExp like `/foo/`or `/^!/` to keep only matching comments.\n\ -\ -Note that currently not *all* comments can be kept when compression is on, \ -because of dead code removal or cascading statements into sequences.") - - .describe("preamble", "Preamble to prepend to the output. You can use this to insert a \ -comment, for example for licensing information. This will not be \ -parsed, but the source map will adjust for its presence.") - - .describe("stats", "Display operations run time on STDERR.") - .describe("acorn", "Use Acorn for parsing.") - .describe("spidermonkey", "Assume input files are SpiderMonkey AST format (as JSON).") - .describe("self", "Build itself (UglifyJS2) as a library (implies --wrap=UglifyJS --export-all)") - .describe("wrap", "Embed everything in a big function, making the “exports” and “global” variables available. \ -You need to pass an argument to this option to specify the name that your module will take when included in, say, a browser.") - .describe("export-all", "Only used when --wrap, this tells UglifyJS to add code to automatically export all globals.") - .describe("lint", "Display some scope warnings") - .describe("v", "Verbose") - .describe("V", "Print version number and exit.") - .describe("noerr", "Don't throw an error for unknown options in -c, -b or -m.") - .describe("bare-returns", "Allow return outside of functions. Useful when minifying CommonJS modules.") - .describe("keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name.") - .describe("quotes", "Quote style (0 - auto, 1 - single, 2 - double, 3 - original)") - .describe("reserved-file", "File containing reserved names") - .describe("reserve-domprops", "Make (most?) DOM properties reserved for --mangle-props") - .describe("mangle-props", "Mangle property names (0 - disabled, 1 - mangle all properties, 2 - mangle unquoted properies)") - .describe("mangle-regex", "Only mangle property names matching the regex") - .describe("name-cache", "File to hold mangled names mappings") - .describe("pure-funcs", "List of functions that can be safely removed if their return value is not used") - .describe("dump-spidermonkey-ast", "Dump SpiderMonkey AST to stdout.") - .describe("wrap-iife", "Wrap IIFEs in parenthesis. Note: this disables the negate_iife compression option") - - .alias("p", "prefix") - .alias("o", "output") - .alias("v", "verbose") - .alias("b", "beautify") - .alias("m", "mangle") - .alias("c", "compress") - .alias("d", "define") - .alias("r", "reserved") - .alias("V", "version") - .alias("e", "enclose") - .alias("q", "quotes") - - .string("source-map") - .string("source-map-root") - .string("source-map-url") - .string("b") - .string("beautify") - .string("m") - .string("mangle") - .string("mangle-props-debug") - .string("c") - .string("compress") - .string("d") - .string("define") - .string("e") - .string("enclose") - .string("comments") - .string("wrap") - .string("p") - .string("prefix") - .string("name-cache") - - .array("reserved-file") - .array("pure-funcs") - - .boolean("expr") - .boolean("source-map-inline") - .boolean("source-map-include-sources") - .boolean("screw-ie8") - .boolean("support-ie8") - .boolean("export-all") - .boolean("self") - .boolean("v") - .boolean("verbose") - .boolean("stats") - .boolean("acorn") - .boolean("spidermonkey") - .boolean("dump-spidermonkey-ast") - .boolean("lint") - .boolean("V") - .boolean("version") - .boolean("noerr") - .boolean("bare-returns") - .boolean("keep-fnames") - .boolean("reserve-domprops") - .boolean("wrap-iife") - - .wrap(80) - - .argv -; - -normalize(ARGS); - -if (ARGS.noerr) { - UglifyJS.DefaultsError.croak = function(msg, defs) { - print_error("WARN: " + msg); - }; -} - -if (ARGS.version || ARGS.V) { - var json = require("../package.json"); - print(json.name + ' ' + json.version); - process.exit(0); -} - -if (ARGS.ast_help) { - var desc = UglifyJS.describe_ast(); - print(typeof desc == "string" ? desc : JSON.stringify(desc, null, 2)); - process.exit(0); -} - -if (ARGS.h || ARGS.help) { - print(yargs.help()); - process.exit(0); -} - -if (ARGS.acorn) { - acorn = require("acorn"); -} - -var COMPRESS = getOptions("c", true); -var MANGLE = getOptions("m", true); -var BEAUTIFY = getOptions("b", true); -var RESERVED = null; - -if (ARGS.reserved_file) ARGS.reserved_file.forEach(function(filename){ - RESERVED = UglifyJS.readReservedFile(filename, RESERVED); +// 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); }); -if (ARGS.reserve_domprops) { - RESERVED = UglifyJS.readDefaultReservedFile(RESERVED); -} +var fs = require("fs"); +var info = require("../package.json"); +var path = require("path"); +var program = require("commander"); +var UglifyJS = require("../tools/node"); -if (ARGS.d) { - if (COMPRESS) COMPRESS.global_defs = getOptions("d"); -} - -if (ARGS.pure_funcs) { - if (COMPRESS) COMPRESS.pure_funcs = ARGS.pure_funcs; -} - -if (ARGS.r) { - if (MANGLE) MANGLE.except = ARGS.r.replace(/^\s+|\s+$/g).split(/\s*,+\s*/); -} - -if (RESERVED && MANGLE) { - if (!MANGLE.except) MANGLE.except = RESERVED.vars; - else MANGLE.except = MANGLE.except.concat(RESERVED.vars); -} - -function readNameCache(key) { - return UglifyJS.readNameCache(ARGS.name_cache, key); -} - -function writeNameCache(key, cache) { - return UglifyJS.writeNameCache(ARGS.name_cache, key, cache); -} - -function extractRegex(str) { - if (/^\/.*\/[a-zA-Z]*$/.test(str)) { - var regex_pos = str.lastIndexOf("/"); - return new RegExp(str.substr(1, regex_pos - 1), str.substr(regex_pos + 1)); - } else { - throw new Error("Invalid regular expression: " + str); - } -} - -if (ARGS.quotes === true) { - ARGS.quotes = 3; -} - -if (ARGS.mangle_props === true) { - ARGS.mangle_props = 1; -} else if (ARGS.mangle_props === "unquoted") { - ARGS.mangle_props = 2; -} - -var OUTPUT_OPTIONS = { - beautify : BEAUTIFY ? true : false, - max_line_len : 32000, - preamble : ARGS.preamble || null, - quote_style : ARGS.quotes != null ? ARGS.quotes : 0, +var files = {}; +var options = { + compress: false, + mangle: false }; - -if (ARGS.mangle_props == 2) { - OUTPUT_OPTIONS.keep_quoted_props = true; - if (COMPRESS && !("properties" in COMPRESS)) - COMPRESS.properties = false; +program._name = info.name; +program.version(info.version); +program.parseArgv = program.parse; +program.parse = undefined; +program.option("-p, --parse ", "Specify parser options.", parse_js("parse", true)); +program.option("-c, --compress [options]", "Enable compressor/specify compressor options.", parse_js("compress", true)); +program.option("-m, --mangle [options]", "Mangle names/specify mangler options.", parse_js("mangle", true)); +program.option("--mangle-props [options]", "Mangle properties/specify mangler options.", parse_js("mangle-props", true)); +program.option("-b, --beautify [options]", "Beautify output/specify output options.", parse_js("beautify", true)); +program.option("-o, --output ", "Output file (default STDOUT)."); +program.option("--comments [filter]", "Preserve copyright comments in the output."); +program.option("--config-file ", "Read minify() options from JSON file."); +program.option("-d, --define [=value]", "Global definitions.", parse_js("define")); +program.option("--ie8", "Support non-standard Internet Explorer 8."); +program.option("--keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name."); +program.option("--name-cache ", "File to hold mangled name mappings."); +program.option("--self", "Build UglifyJS2 as a library (implies --wrap UglifyJS)"); +program.option("--source-map [options]", "Enable source map/specify source map options.", parse_source_map()); +program.option("--stats", "Display operations run time on STDERR.") +program.option("--toplevel", "Compress and/or mangle variables in toplevel scope."); +program.option("--verbose", "Print diagnostic messages."); +program.option("--warn", "Print warning messages."); +program.option("--wrap ", "Embed everything as a function with “exports” corresponding to “name” globally."); +program.arguments("[files...]").parseArgv(process.argv); +if (program.configFile) { + options = JSON.parse(read_file(program.configFile)); } - -if (ARGS.support_ie8 === true && ARGS.screw_ie8 !== true) { - screw_ie8 = false; +if (!program.output && program.sourceMap && program.sourceMap.url != "inline") { + fatal("ERROR: cannot write source map to STDOUT"); } - -if (COMPRESS) COMPRESS.screw_ie8 = screw_ie8; -if (MANGLE) MANGLE.screw_ie8 = screw_ie8; -OUTPUT_OPTIONS.screw_ie8 = screw_ie8; - -if (ARGS.keep_fnames) { - if (COMPRESS) COMPRESS.keep_fnames = true; - if (MANGLE) MANGLE.keep_fnames = true; +[ + "compress", + "ie8", + "mangle", + "sourceMap", + "toplevel", + "wrap" +].forEach(function(name) { + if (name in program) { + options[name] = program[name]; + } +}); +if (program.beautify) { + options.output = typeof program.beautify == "object" ? program.beautify : {}; + if (!("beautify" in options.output)) { + options.output.beautify = true; + } } - -if (ARGS.wrap_iife) { - if (COMPRESS) COMPRESS.negate_iife = false; - OUTPUT_OPTIONS.wrap_iife = true; +if (program.comments) { + if (typeof options.output != "object") options.output = {}; + options.output.comments = typeof program.comments == "string" ? program.comments : "some"; } - -if (BEAUTIFY) - UglifyJS.merge(OUTPUT_OPTIONS, BEAUTIFY); - -if (ARGS.comments === "") { - OUTPUT_OPTIONS.comments = "some"; +if (program.define) { + if (typeof options.compress != "object") options.compress = {}; + if (typeof options.compress.global_defs != "object") options.compress.global_defs = {}; + for (var expr in program.define) { + options.compress.global_defs[expr] = program.define[expr]; + } +} +if (program.keepFnames) { + options.keep_fnames = true; +} +if (program.mangleProps) { + if (program.mangleProps.domprops) { + delete program.mangleProps.domprops; + } else { + if (typeof program.mangleProps != "object") program.mangleProps = {}; + if (!Array.isArray(program.mangleProps.reserved)) program.mangleProps.reserved = []; + require("../tools/domprops").forEach(function(name) { + UglifyJS.push_uniq(program.mangleProps.reserved, name); + }); + } + if (typeof options.mangle != "object") options.mangle = {}; + options.mangle.properties = program.mangleProps; +} +var cache; +if (program.nameCache) { + cache = JSON.parse(read_file(program.nameCache, "{}")); + if (options.mangle) { + if (typeof options.mangle != "object") options.mangle = {}; + options.mangle.cache = to_cache("vars"); + if (options.mangle.properties) { + if (typeof options.mangle.properties != "object") options.mangle.properties = {}; + options.mangle.properties.cache = to_cache("props"); + } + } +} +if (program.parse) { + if (program.parse.acorn || program.parse.spidermonkey) { + if (program.sourceMap) fatal("ERROR: inline source map only works with built-in parser"); + } else { + options.parse = program.parse; + } +} +var convert_path = function(name) { + return name; +}; +if (typeof program.sourceMap == "object" && "base" in program.sourceMap) { + convert_path = function() { + var base = program.sourceMap.base; + delete options.sourceMap.base; + return function(name) { + return path.relative(base, name); + }; + }(); +} +if (program.verbose) { + options.warnings = "verbose"; +} else if (program.warn) { + options.warnings = true; +} +if (program.self) { + if (program.args.length) { + console.error("WARN: Ignoring input files since --self was passed"); + } + if (!options.wrap) options.wrap = "UglifyJS"; + simple_glob(UglifyJS.FILES).forEach(function(name) { + files[convert_path(name)] = read_file(name); + }); + run(); +} else if (program.args.length) { + simple_glob(program.args).forEach(function(name) { + files[convert_path(name)] = read_file(name); + }); + run(); } else { - OUTPUT_OPTIONS.comments = ARGS.comments; + var chunks = []; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", function(chunk) { + chunks.push(chunk); + }).on("end", function() { + files = [ chunks.join("") ]; + run(); + }); + process.stdin.resume(); } -var files = ARGS._.slice(); - -if (process.platform === "win32") - files = UglifyJS.simple_glob(files); - -if (ARGS.self) { - if (files.length > 0) { - print_error("WARN: Ignoring input files since --self was passed"); - } - files = UglifyJS.FILES; - if (!ARGS.wrap) ARGS.wrap = "UglifyJS"; +function convert_ast(fn) { + return UglifyJS.AST_Node.from_mozilla_ast(Object.keys(files).reduce(fn, null)); } -var ORIG_MAP = ARGS.in_source_map; - -if (ORIG_MAP && ORIG_MAP != "inline") { - ORIG_MAP = JSON.parse(fs.readFileSync(ORIG_MAP)); - if (files.length == 0) { - print_error("INFO: Using file from the input source map: " + ORIG_MAP.file); - files = [ ORIG_MAP.file ]; - } -} - -if (files.length == 0) { - files = [ "-" ]; -} - -if (ORIG_MAP == "inline") { - if (files.length > 1) { - print_error("ERROR: Inline source map only works with singular input"); - process.exit(1); - } - if (ARGS.acorn || ARGS.spidermonkey) { - print_error("ERROR: Inline source map only works with built-in parser"); - process.exit(1); - } -} - -if (files.indexOf("-") >= 0 && ARGS.source_map) { - print_error("ERROR: Source map doesn't work with input from STDIN"); - process.exit(1); -} - -if (files.filter(function(el){ return el == "-" }).length > 1) { - print_error("ERROR: Can read a single file from STDIN (two or more dashes specified)"); - process.exit(1); -} - -var STATS = {}; -var TOPLEVEL = null; -var P_RELATIVE = ARGS.p && ARGS.p == "relative"; -var SOURCES_CONTENT = {}; -var index = 0; - -!function cb() { - if (index == files.length) return done(); - var file = files[index++]; - read_whole_file(file, function (err, code) { - if (err) { - print_error("ERROR: can't read file: " + file); - process.exit(1); - } - if (ORIG_MAP == "inline") { - ORIG_MAP = read_source_map(code); - } - if (ARGS.p != null) { - if (P_RELATIVE) { - file = path.relative(path.dirname(ARGS.source_map), file).replace(/\\/g, '/'); - } else { - var p = parseInt(ARGS.p, 10); - if (!isNaN(p)) { - file = file.replace(/^\/+/, "").split(/\/+/).slice(ARGS.p).join("/"); - } - } - } - SOURCES_CONTENT[file] = code; - time_it("parse", function(){ - if (ARGS.spidermonkey) { - var program = JSON.parse(code); - if (!TOPLEVEL) TOPLEVEL = program; - else TOPLEVEL.body = TOPLEVEL.body.concat(program.body); - } - else if (ARGS.acorn) { - TOPLEVEL = acorn.parse(code, { - locations : true, - sourceFile : file, - program : TOPLEVEL +function run() { + UglifyJS.AST_Node.warn_function = function(msg) { + console.error("WARN:", msg); + }; + if (program.stats) program.stats = Date.now(); + try { + if (program.parse) { + if (program.parse.acorn) { + files = convert_ast(function(toplevel, name) { + return require("acorn").parse(files[name], { + locations: true, + program: toplevel, + sourceFile: name + }); + }); + } else if (program.parse.spidermonkey) { + files = convert_ast(function(toplevel, name) { + var obj = JSON.parse(files[name]); + if (!toplevel) return obj; + toplevel.body = toplevel.body.concat(obj.body); + return toplevel; }); } - else { - try { - TOPLEVEL = UglifyJS.parse(code, { - filename : file, - toplevel : TOPLEVEL, - expression : ARGS.expr, - bare_returns : ARGS.bare_returns, - }); - } catch(ex) { - if (ex instanceof UglifyJS.JS_Parse_Error) { - print_error("Parse error at " + file + ":" + ex.line + "," + ex.col); - var col = ex.col; - var line = code.split(/\r?\n/)[ex.line - (col ? 1 : 2)]; - if (line) { - if (col > 40) { - line = line.slice(col - 40); - col = 40; - } - if (col) { - print_error(line.slice(0, 80)); - print_error(line.slice(0, col).replace(/\S/g, " ") + "^"); - } else { - print_error(line.slice(-40)); - print_error(line.slice(-40).replace(/\S/g, " ") + "^"); - } - } - print_error(ex.stack); - process.exit(1); - } - throw ex; + } + var result = UglifyJS.minify(files, options); + } catch (ex) { + if (ex instanceof UglifyJS.JS_Parse_Error) { + console.error("Parse error at " + ex.filename + ":" + ex.line + "," + ex.col); + var col = ex.col; + var line = files[ex.filename].split(/\r?\n/)[ex.line - (col ? 1 : 2)]; + if (line) { + if (col > 40) { + line = line.slice(col - 40); + col = 40; } - }; - }); - cb(); - }); -}(); + if (col) { + console.error(line.slice(0, 80)); + console.error(line.slice(0, col).replace(/\S/g, " ") + "^"); + } else { + console.error(line.slice(-40)); + console.error(line.slice(-40).replace(/\S/g, " ") + "^"); + } + } + } + fatal("ERROR: " + ex.message); + } + if (program.output == "spidermonkey") { + console.log(JSON.stringify(UglifyJS.parse(result.code).to_mozilla_ast(), null, 2)); + } else if (program.output) { + fs.writeFileSync(program.output, result.code); + if (result.map) { + fs.writeFileSync(program.output + ".map", result.map); + } + } else { + console.log(result.code); + } + if (program.nameCache) { + fs.writeFileSync(program.nameCache, JSON.stringify(cache, function(key, value) { + return value instanceof UglifyJS.Dictionary ? value.toObject() : value; + })); + } + if (program.stats) console.error("Elapsed:", Date.now() - program.stats); +} -function done() { - var OUTPUT_FILE = ARGS.o; +function fatal(message) { + console.error(message); + process.exit(1); +} - var SOURCE_MAP = (ARGS.source_map || ARGS.source_map_inline) ? UglifyJS.SourceMap({ - file: P_RELATIVE ? path.relative(path.dirname(ARGS.source_map), OUTPUT_FILE) : OUTPUT_FILE, - root: ARGS.source_map_root || ORIG_MAP && ORIG_MAP.sourceRoot, - orig: ORIG_MAP, - }) : null; - - OUTPUT_OPTIONS.source_map = SOURCE_MAP; +// A file glob function that only supports "*" and "?" wildcards in the basename. +// Example: "foo/bar/*baz??.*.js" +// Argument `glob` may be a string or an array of strings. +// Returns an array of strings. Garbage in, garbage out. +function simple_glob(glob) { + if (Array.isArray(glob)) { + return [].concat.apply([], glob.map(simple_glob)); + } + if (glob.match(/\*|\?/)) { + var dir = path.dirname(glob); + try { + var entries = fs.readdirSync(dir); + } catch (ex) {} + if (entries) { + var pattern = "^" + path.basename(glob) + .replace(/[.+^$[\]\\(){}]/g, "\\$&") + .replace(/\*/g, "[^/\\\\]*") + .replace(/\?/g, "[^/\\\\]") + "$"; + var mod = process.platform === "win32" ? "i" : ""; + var rx = new RegExp(pattern, mod); + var results = entries.filter(function(name) { + return rx.test(name); + }).map(function(name) { + return path.join(dir, name); + }); + if (results.length) return results; + } + } + return [ glob ]; +} +function read_file(path, default_value) { try { - var output = UglifyJS.OutputStream(OUTPUT_OPTIONS); - var compressor = COMPRESS && UglifyJS.Compressor(COMPRESS); - } catch(ex) { - if (ex instanceof UglifyJS.DefaultsError) { - print_error(ex.message); - print_error("Supported options:"); - print_error(sys.inspect(ex.defs)); - process.exit(1); - } + return fs.readFileSync(path, "utf8"); + } catch (ex) { + if (ex.code == "ENOENT" && default_value != null) return default_value; + fatal("ERROR: " + ex.message); } +} - if (ARGS.acorn || ARGS.spidermonkey) time_it("convert_ast", function(){ - TOPLEVEL = UglifyJS.AST_Node.from_mozilla_ast(TOPLEVEL); - }); - - if (ARGS.wrap != null) { - TOPLEVEL = TOPLEVEL.wrap_commonjs(ARGS.wrap, ARGS.export_all); - } - - if (ARGS.enclose != null) { - var arg_parameter_list = ARGS.enclose; - if (arg_parameter_list === true) { - arg_parameter_list = []; - } - else if (!(arg_parameter_list instanceof Array)) { - arg_parameter_list = [arg_parameter_list]; - } - TOPLEVEL = TOPLEVEL.wrap_enclose(arg_parameter_list); - } - - if (ARGS.mangle_props || ARGS.name_cache) (function(){ - var reserved = RESERVED ? RESERVED.props : null; - var cache = readNameCache("props"); - var regex; - +function parse_js(flag, constants) { + return function(value, options) { + options = options || {}; try { - regex = ARGS.mangle_regex ? extractRegex(ARGS.mangle_regex) : null; - } catch (e) { - print_error("ERROR: Invalid --mangle-regex: " + e.message); - process.exit(1); - } + UglifyJS.parse(value, { + expression: true + }).walk(new UglifyJS.TreeWalker(function(node) { + if (node instanceof UglifyJS.AST_Assign) { + var name = node.left.print_to_string(); + var value = node.right; + if (!constants) { + options[name] = value; + } else if (value instanceof UglifyJS.AST_Array) { + options[name] = value.elements.map(to_string); + } else { + options[name] = to_string(value); + } + return true; + } + if (node instanceof UglifyJS.AST_Symbol || node instanceof UglifyJS.AST_PropAccess) { + var name = node.print_to_string(); + options[name] = true; + return true; + } + if (!(node instanceof UglifyJS.AST_Sequence)) throw node; - TOPLEVEL = UglifyJS.mangle_properties(TOPLEVEL, { - reserved : reserved, - cache : cache, - only_cache : !ARGS.mangle_props, - regex : regex, - ignore_quoted : ARGS.mangle_props == 2, - debug : typeof ARGS.mangle_props_debug === "undefined" ? false : ARGS.mangle_props_debug - }); - writeNameCache("props", cache); - })(); - - var SCOPE_IS_NEEDED = COMPRESS || MANGLE || ARGS.lint - var TL_CACHE = readNameCache("vars"); - if (MANGLE) MANGLE.cache = TL_CACHE; - - if (SCOPE_IS_NEEDED) { - time_it("scope", function(){ - TOPLEVEL.figure_out_scope(MANGLE || { screw_ie8: screw_ie8, cache: TL_CACHE }); - if (ARGS.lint) { - TOPLEVEL.scope_warnings(); - } - }); - } - - if (COMPRESS) { - time_it("squeeze", function(){ - TOPLEVEL = compressor.compress(TOPLEVEL); - }); - } - - if (SCOPE_IS_NEEDED) { - time_it("scope", function(){ - TOPLEVEL.figure_out_scope(MANGLE || { screw_ie8: screw_ie8, cache: TL_CACHE }); - if (MANGLE && !TL_CACHE) { - TOPLEVEL.compute_char_frequency(MANGLE); - } - }); - } - - if (MANGLE) time_it("mangle", function(){ - TOPLEVEL.mangle_names(MANGLE); - }); - - writeNameCache("vars", TL_CACHE); - - if (ARGS.source_map_include_sources) { - for (var file in SOURCES_CONTENT) { - if (SOURCES_CONTENT.hasOwnProperty(file)) { - SOURCE_MAP.get().setSourceContent(file, SOURCES_CONTENT[file]); - } - } - } - - if (ARGS.dump_spidermonkey_ast) { - print(JSON.stringify(TOPLEVEL.to_mozilla_ast(), null, 2)); - } else { - time_it("generate", function(){ - TOPLEVEL.print(output); - }); - - output = output.get(); - - if (SOURCE_MAP) { - if (ARGS.source_map_inline) { - var base64_string = new Buffer(SOURCE_MAP.toString()).toString('base64'); - output += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + base64_string; - } else { - fs.writeFileSync(ARGS.source_map, SOURCE_MAP, "utf8"); - var source_map_url = ARGS.source_map_url || ( - P_RELATIVE - ? path.relative(path.dirname(OUTPUT_FILE), ARGS.source_map) - : ARGS.source_map - ); - output += "\n//# sourceMappingURL=" + source_map_url; - } - } - - if (OUTPUT_FILE) { - fs.writeFileSync(OUTPUT_FILE, output, "utf8"); - } else { - print(output); - } - } - - if (ARGS.stats) { - print_error(UglifyJS.string_template("Timing information (compressed {count} files):", { - count: files.length - })); - for (var i in STATS) if (STATS.hasOwnProperty(i)) { - print_error(UglifyJS.string_template("- {name}: {time}s", { - name: i, - time: (STATS[i] / 1000).toFixed(3) + function to_string(value) { + return value instanceof UglifyJS.AST_Constant ? value.getValue() : value.print_to_string({ + quote_keys: true + }); + } })); - } - } -} - -/* -----[ functions ]----- */ - -function normalize(o) { - for (var i in o) if (o.hasOwnProperty(i) && /-/.test(i)) { - o[i.replace(/-/g, "_")] = o[i]; - delete o[i]; - } -} - -function getOptions(flag, constants) { - var x = ARGS[flag]; - if (x == null || x === false) return null; - var ret = {}; - if (x !== "") { - if (Array.isArray(x)) x = x.map(function (v) { return "(" + v + ")"; }).join(", "); - - var ast; - try { - ast = UglifyJS.parse(x, { cli: true, expression: true }); } catch(ex) { - if (ex instanceof UglifyJS.JS_Parse_Error) { - print_error("Error parsing arguments for flag `" + flag + "': " + x); - process.exit(1); - } + fatal("Error parsing arguments for '" + flag + "': " + value); } - ast.walk(new UglifyJS.TreeWalker(function(node){ - if (node instanceof UglifyJS.AST_Sequence) return; // descend - if (node instanceof UglifyJS.AST_Assign) { - var name = node.left.print_to_string().replace(/-/g, "_"); - var value = node.right; - if (constants) - value = new Function("return (" + value.print_to_string() + ")")(); - ret[name] = value; - return true; // no descend - } - if (node instanceof UglifyJS.AST_Symbol || node instanceof UglifyJS.AST_Binary) { - var name = node.print_to_string().replace(/-/g, "_"); - ret[name] = true; - return true; // no descend - } - print_error(node.TYPE); - print_error("Error parsing arguments for flag `" + flag + "': " + x); - process.exit(1); - })); + return options; } - return ret; } -function read_whole_file(filename, cb) { - if (filename == "-") { - var chunks = []; - process.stdin.setEncoding('utf-8'); - process.stdin.on('data', function (chunk) { - chunks.push(chunk); - }).on('end', function () { - cb(null, chunks.join("")); - }); - process.openStdin(); +function parse_source_map() { + var parse = parse_js("sourceMap", true); + return function(value, options) { + var hasContent = options && options.sourceMap && "content" in options.sourceMap; + var settings = parse(value, options); + if (!hasContent && settings.content && settings.content != "inline") { + console.error("INFO: Using input source map:", settings.content); + settings.content = read_file(settings.content, settings.content); + } + return settings; + } +} + +function to_cache(key) { + if (cache[key]) { + cache[key].props = UglifyJS.Dictionary.fromObject(cache[key].props); } else { - fs.readFile(filename, "utf-8", cb); + cache[key] = { + cname: -1, + props: new UglifyJS.Dictionary() + }; } -} - -function read_source_map(code) { - var match = /\n\/\/# sourceMappingURL=data:application\/json(;.*?)?;base64,(.*)/.exec(code); - if (!match) { - print_error("WARN: inline source map not found"); - return null; - } - return JSON.parse(new Buffer(match[2], "base64")); -} - -function time_it(name, cont) { - var t1 = new Date().getTime(); - var ret = cont(); - if (ARGS.stats) { - var spent = new Date().getTime() - t1; - if (STATS[name]) STATS[name] += spent; - else STATS[name] = spent; - } - return ret; -} - -function print_error(msg) { - console.error("%s", msg); -} - -function print(txt) { - console.log("%s", txt); + return cache[key]; } diff --git a/lib/ast.js b/lib/ast.js index f78ac576..0fa051b8 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -325,62 +325,13 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { $propdoc: { globals: "[Object/S] a map of name -> SymbolDef for all undeclared names", }, - wrap_enclose: function(arg_parameter_pairs) { - var self = this; - var args = []; - var parameters = []; - - arg_parameter_pairs.forEach(function(pair) { - var splitAt = pair.lastIndexOf(":"); - - args.push(pair.substr(0, splitAt)); - parameters.push(pair.substr(splitAt + 1)); - }); - - var wrapped_tl = "(function(" + parameters.join(",") + "){ '$ORIG'; })(" + args.join(",") + ")"; + wrap_commonjs: function(name) { + var body = this.body; + var wrapped_tl = "(function(exports){'$ORIG';})(typeof " + name + "=='undefined'?(" + name + "={}):" + name + ");"; wrapped_tl = parse(wrapped_tl); wrapped_tl = wrapped_tl.transform(new TreeTransformer(function before(node){ if (node instanceof AST_Directive && node.value == "$ORIG") { - return MAP.splice(self.body); - } - })); - return wrapped_tl; - }, - wrap_commonjs: function(name, export_all) { - var self = this; - var to_export = []; - if (export_all) { - self.figure_out_scope(); - self.walk(new TreeWalker(function(node){ - if (node instanceof AST_SymbolDeclaration && node.definition().global) { - if (!find_if(function(n){ return n.name == node.name }, to_export)) - to_export.push(node); - } - })); - } - var wrapped_tl = "(function(exports, global){ '$ORIG'; '$EXPORTS'; global['" + name + "'] = exports; }({}, (function(){return this}())))"; - wrapped_tl = parse(wrapped_tl); - wrapped_tl = wrapped_tl.transform(new TreeTransformer(function before(node){ - if (node instanceof AST_Directive) { - switch (node.value) { - case "$ORIG": - return MAP.splice(self.body); - case "$EXPORTS": - var body = []; - to_export.forEach(function(sym){ - body.push(new AST_SimpleStatement({ - body: new AST_Assign({ - left: new AST_Sub({ - expression: new AST_SymbolRef({ name: "exports" }), - property: new AST_String({ value: sym.name }), - }), - operator: "=", - right: new AST_SymbolRef(sym), - }), - })); - }); - return MAP.splice(body); - } + return MAP.splice(body); } })); return wrapped_tl; @@ -929,7 +880,7 @@ TreeWalker.prototype = { parent: function(n) { return this.stack[this.stack.length - 2 - (n || 0)]; }, - push: function (node) { + push: function(node) { if (node instanceof AST_Lambda) { this.directives = Object.create(this.directives); } else if (node instanceof AST_Directive && !this.directives[node.value]) { diff --git a/lib/compress.js b/lib/compress.js index 5128abb2..c8b15ffc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -61,6 +61,7 @@ function Compressor(options, false_by_default) { global_defs : {}, hoist_funs : !false_by_default, hoist_vars : false, + ie8 : false, if_return : !false_by_default, join_vars : !false_by_default, keep_fargs : true, @@ -73,7 +74,6 @@ function Compressor(options, false_by_default) { pure_getters : !false_by_default && "strict", pure_funcs : null, reduce_vars : !false_by_default, - screw_ie8 : true, sequences : !false_by_default, side_effects : !false_by_default, switches : !false_by_default, @@ -84,7 +84,7 @@ function Compressor(options, false_by_default) { unsafe_math : false, unsafe_proto : false, unused : !false_by_default, - warnings : true, + warnings : false, }, true); var pure_funcs = this.options["pure_funcs"]; if (typeof pure_funcs == "function") { @@ -1138,7 +1138,7 @@ merge(Compressor.prototype, { /* -----[ boolean/negation helpers ]----- */ // methods to determine whether an expression has a boolean result type - (function (def){ + (function(def){ var unary_bool = [ "!", "delete" ]; var binary_bool = [ "in", "instanceof", "==", "!=", "===", "!==", "<", "<=", ">=", ">" ]; def(AST_Node, return_false); @@ -1166,7 +1166,7 @@ merge(Compressor.prototype, { }); // methods to determine if an expression has a numeric result type - (function (def){ + (function(def){ def(AST_Node, return_false); def(AST_Number, return_true); var unary = makePredicate("+ - ~ ++ --"); @@ -1194,7 +1194,7 @@ merge(Compressor.prototype, { }); // methods to determine if an expression has a string result type - (function (def){ + (function(def){ def(AST_Node, return_false); def(AST_String, return_true); def(AST_UnaryPrefix, function(){ @@ -1224,7 +1224,7 @@ merge(Compressor.prototype, { if (parent instanceof AST_Assign && parent.left === node) return node; } - (function (def){ + (function(def){ AST_Node.DEFMETHOD("resolve_defines", function(compressor) { if (!compressor.option("global_defs")) return; var def = this._find_defs(compressor, ""); @@ -1305,7 +1305,7 @@ merge(Compressor.prototype, { } // methods to evaluate a constant expression - (function (def){ + (function(def){ // If the node has been successfully reduced to a constant, // then its value is returned; otherwise the element itself // is returned. @@ -2767,10 +2767,11 @@ merge(Compressor.prototype, { return arg.value; }).join(",") + "){" + self.args[self.args.length - 1].value + "})()"; var ast = parse(code); - ast.figure_out_scope({ screw_ie8: compressor.option("screw_ie8") }); + var mangle = { ie8: compressor.option("ie8") }; + ast.figure_out_scope(mangle); var comp = new Compressor(compressor.options); ast = ast.transform(comp); - ast.figure_out_scope({ screw_ie8: compressor.option("screw_ie8") }); + ast.figure_out_scope(mangle); ast.mangle_names(); var fun; try { @@ -3216,7 +3217,7 @@ merge(Compressor.prototype, { && self.right.operator == "typeof") { var expr = self.right.expression; if (expr instanceof AST_SymbolRef ? !expr.undeclared() - : !(expr instanceof AST_PropAccess) || compressor.option("screw_ie8")) { + : !(expr instanceof AST_PropAccess && compressor.option("ie8"))) { self.right = expr; self.left = make_node(AST_Undefined, self.left).optimize(compressor); if (self.operator.length == 2) self.operator += "="; @@ -3540,7 +3541,7 @@ merge(Compressor.prototype, { return def.optimize(compressor); } // testing against !self.scope.uses_with first is an optimization - if (compressor.option("screw_ie8") + if (!compressor.option("ie8") && self.undeclared() && (!self.scope.uses_with || !compressor.find_parent(AST_With))) { switch (self.name) { @@ -3860,7 +3861,7 @@ merge(Compressor.prototype, { var prop = self.property; if (prop instanceof AST_String && compressor.option("properties")) { prop = prop.getValue(); - if (RESERVED_WORDS(prop) ? compressor.option("screw_ie8") : is_identifier_string(prop)) { + if (RESERVED_WORDS(prop) ? !compressor.option("ie8") : is_identifier_string(prop)) { return make_node(AST_Dot, self, { expression : self.expression, property : prop @@ -3887,7 +3888,7 @@ merge(Compressor.prototype, { return def.optimize(compressor); } var prop = self.property; - if (RESERVED_WORDS(prop) && !compressor.option("screw_ie8")) { + if (RESERVED_WORDS(prop) && compressor.option("ie8")) { return make_node(AST_Sub, self, { expression : self.expression, property : make_node(AST_String, self, { diff --git a/lib/minify.js b/lib/minify.js new file mode 100644 index 00000000..fe762db6 --- /dev/null +++ b/lib/minify.js @@ -0,0 +1,146 @@ +"use strict"; + +var to_ascii = typeof atob == "undefined" ? function(b64) { + return new Buffer(b64, "base64").toString(); +} : atob; +var to_base64 = typeof btoa == "undefined" ? function(str) { + return new Buffer(str).toString("base64"); +} : btoa; + +function read_source_map(code) { + var match = /\n\/\/# sourceMappingURL=data:application\/json(;.*?)?;base64,(.*)/.exec(code); + if (!match) { + AST_Node.warn("inline source map not found"); + return null; + } + return to_ascii(match[2]); +} + +function set_shorthand(name, options, keys) { + if (options[name]) { + keys.forEach(function(key) { + if (options[key]) { + if (typeof options[key] != "object") options[key] = {}; + if (!(name in options[key])) options[key][name] = options[name]; + } + }); + } +} + +function minify(files, options) { + var warn_function = AST_Node.warn_function; + try { + if (typeof files == "string") { + files = [ files ]; + } + options = defaults(options, { + compress: {}, + ie8: false, + keep_fnames: false, + mangle: {}, + output: {}, + parse: {}, + sourceMap: false, + toplevel: false, + warnings: false, + wrap: false, + }, true); + set_shorthand("ie8", options, [ "compress", "mangle", "output" ]); + set_shorthand("keep_fnames", options, [ "compress", "mangle" ]); + set_shorthand("toplevel", options, [ "compress", "mangle" ]); + set_shorthand("warnings", options, [ "compress" ]); + if (options.mangle) { + options.mangle = defaults(options.mangle, { + cache: null, + eval: false, + ie8: false, + keep_fnames: false, + properties: false, + reserved: [], + toplevel: false, + }, true); + } + if (options.sourceMap) { + options.sourceMap = defaults(options.sourceMap, { + content: null, + filename: null, + includeSources: false, + root: null, + url: null, + }, true); + } + var warnings = []; + if (options.warnings && !AST_Node.warn_function) { + AST_Node.warn_function = function(warning) { + warnings.push(warning); + }; + } + var toplevel; + if (files instanceof AST_Toplevel) { + toplevel = files; + } else { + options.parse = options.parse || {}; + options.parse.toplevel = null; + for (var name in files) { + options.parse.filename = name; + options.parse.toplevel = parse(files[name], options.parse); + if (options.sourceMap && options.sourceMap.content == "inline") { + if (Object.keys(files).length > 1) + throw new Error("inline source map only works with singular input"); + options.sourceMap.content = read_source_map(files[name]); + } + } + toplevel = options.parse.toplevel; + } + if (options.wrap) { + toplevel = toplevel.wrap_commonjs(options.wrap); + } + if (options.compress) { + toplevel.figure_out_scope(options.mangle); + toplevel = new Compressor(options.compress).compress(toplevel); + } + if (options.mangle) { + toplevel.figure_out_scope(options.mangle); + base54.reset(); + toplevel.compute_char_frequency(options.mangle); + toplevel.mangle_names(options.mangle); + if (options.mangle.properties) { + toplevel = mangle_properties(toplevel, options.mangle.properties); + } + } + if (options.sourceMap) { + if (typeof options.sourceMap.content == "string") { + options.sourceMap.content = JSON.parse(options.sourceMap.content); + } + options.output.source_map = SourceMap({ + file: options.sourceMap.filename, + orig: options.sourceMap.content, + root: options.sourceMap.root + }); + if (options.sourceMap.includeSources) { + for (var name in files) { + options.output.source_map.get().setSourceContent(name, files[name]); + } + } + } + var stream = OutputStream(options.output); + toplevel.print(stream); + var result = { + code: stream.get() + }; + if (options.sourceMap) { + result.map = options.output.source_map.toString(); + if (options.sourceMap.url == "inline") { + result.code += "\n//# sourceMappingURL=data:application/json;charset=utf-8;base64," + to_base64(result.map); + } else if (options.sourceMap.url) { + result.code += "\n//# sourceMappingURL=" + options.sourceMap.url; + } + } + if (warnings.length) { + result.warnings = warnings; + } + return result; + } finally { + AST_Node.warn_function = warn_function; + } +} diff --git a/lib/output.js b/lib/output.js index fe982a7b..7a2e850f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -57,6 +57,7 @@ function OutputStream(options) { beautify : false, bracketize : false, comments : false, + ie8 : false, indent_level : 4, indent_start : 0, inline_script : true, @@ -66,7 +67,6 @@ function OutputStream(options) { preserve_line : false, quote_keys : false, quote_style : 0, - screw_ie8 : true, semicolons : true, shebang : true, source_map : null, @@ -136,7 +136,7 @@ function OutputStream(options) { case "\t": return "\\t"; case "\b": return "\\b"; case "\f": return "\\f"; - case "\x0B": return options.screw_ie8 ? "\\v" : "\\x0B"; + case "\x0B": return options.ie8 ? "\\x0B" : "\\v"; case "\u2028": return "\\u2028"; case "\u2029": return "\\u2029"; case "\ufeff": return "\\ufeff"; @@ -681,7 +681,7 @@ function OutputStream(options) { } }); - PARENS([ AST_Assign, AST_Conditional ], function (output){ + PARENS([ AST_Assign, AST_Conditional ], function(output){ var p = output.parent(); // !(a = false) → true if (p instanceof AST_Unary) @@ -906,7 +906,7 @@ function OutputStream(options) { function make_then(self, output) { var b = self.body; if (output.option("bracketize") - || !output.option("screw_ie8") && b instanceof AST_Do) + || output.option("ie8") && b instanceof AST_Do) return make_block(b, output); // The squeezer replaces "block"-s that contain only a single // statement with the statement itself; technically, the AST @@ -1222,7 +1222,7 @@ function OutputStream(options) { && +key + "" == key) && parseFloat(key) >= 0) { output.print(make_num(key)); - } else if (RESERVED_WORDS(key) ? output.option("screw_ie8") : is_identifier_string(key)) { + } else if (RESERVED_WORDS(key) ? !output.option("ie8") : is_identifier_string(key)) { if (quote && output.option("keep_quoted_props")) { output.print_string(key, quote); } else { diff --git a/lib/parse.js b/lib/parse.js index c7d75802..27351b53 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -689,7 +689,6 @@ function parse($TEXT, options) { options = defaults(options, { bare_returns : false, - cli : false, expression : false, filename : null, html5_comments : true, @@ -1502,7 +1501,6 @@ function parse($TEXT, options) { }; function is_assignable(expr) { - if (options.cli) return true; return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef; }; diff --git a/lib/propmangle.js b/lib/propmangle.js index aaf5936f..efb31cc1 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -43,16 +43,16 @@ "use strict"; -function find_builtins() { +function find_builtins(reserved) { // NaN will be included due to Number.NaN - var a = [ + [ "null", "true", "false", "Infinity", "-Infinity", "undefined", - ]; + ].forEach(add); [ Object, Array, Function, Number, String, Boolean, Error, Math, Date, RegExp @@ -63,24 +63,23 @@ function find_builtins() { } }); function add(name) { - push_uniq(a, name); + push_uniq(reserved, name); } - return a; } function mangle_properties(ast, options) { options = defaults(options, { + builtins: false, cache: null, debug: false, - ignore_quoted: false, + keep_quoted: false, only_cache: false, regex: null, reserved: null, }); - var reserved = options.reserved; - if (reserved == null) - reserved = find_builtins(); + var reserved = options.reserved || []; + if (!options.builtins) find_builtins(reserved); var cache = options.cache; if (cache == null) { @@ -91,12 +90,12 @@ function mangle_properties(ast, options) { } var regex = options.regex; - var ignore_quoted = options.ignore_quoted; + var keep_quoted = options.keep_quoted; // note debug is either false (disabled), or a string of the debug suffix to use (enabled). // note debug may be enabled as an empty string, which is falsey. Also treat passing 'true' // the same as passing an empty string. - var debug = (options.debug !== false); + var debug = options.debug !== false; var debug_name_suffix; if (debug) { debug_name_suffix = (options.debug === true ? "" : options.debug); @@ -104,12 +103,12 @@ function mangle_properties(ast, options) { var names_to_mangle = []; var unmangleable = []; - var ignored = {}; + var to_keep = {}; // step 1: find candidates to mangle ast.walk(new TreeWalker(function(node){ if (node instanceof AST_ObjectKeyVal) { - add(node.key, ignore_quoted && node.quote); + add(node.key, keep_quoted && node.quote); } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above @@ -119,14 +118,14 @@ function mangle_properties(ast, options) { add(node.property); } else if (node instanceof AST_Sub) { - addStrings(node.property, ignore_quoted); + addStrings(node.property, keep_quoted); } })); // step 2: transform the tree, renaming properties return ast.transform(new TreeTransformer(function(node){ if (node instanceof AST_ObjectKeyVal) { - if (!(ignore_quoted && node.quote)) + if (!(keep_quoted && node.quote)) node.key = mangle(node.key); } else if (node instanceof AST_ObjectProperty) { @@ -137,7 +136,7 @@ function mangle_properties(ast, options) { node.property = mangle(node.property); } else if (node instanceof AST_Sub) { - if (!ignore_quoted) + if (!keep_quoted) node.property = mangleStrings(node.property); } // else if (node instanceof AST_String) { @@ -167,16 +166,16 @@ function mangle_properties(ast, options) { } function should_mangle(name) { - if (ignore_quoted && name in ignored) return false; + if (keep_quoted && name in to_keep) return false; if (regex && !regex.test(name)) return false; if (reserved.indexOf(name) >= 0) return false; return cache.props.has(name) || names_to_mangle.indexOf(name) >= 0; } - function add(name, ignore) { - if (ignore) { - ignored[name] = true; + function add(name, keep) { + if (keep) { + to_keep[name] = true; return; } @@ -199,19 +198,19 @@ function mangle_properties(ast, options) { // debug mode: use a prefix and suffix to preserve readability, e.g. o.foo -> o._$foo$NNN_. var debug_mangled = "_$" + name + "$" + debug_name_suffix + "_"; - if (can_mangle(debug_mangled) && !(ignore_quoted && debug_mangled in ignored)) { + if (can_mangle(debug_mangled) && !(keep_quoted && debug_mangled in to_keep)) { mangled = debug_mangled; } } // either debug mode is off, or it is on and we could not use the mangled name if (!mangled) { - // note can_mangle() does not check if the name collides with the 'ignored' set - // (filled with quoted properties when ignore_quoted set). Make sure we add this + // Note: `can_mangle()` does not check if the name collides with the `to_keep` set + // (filled with quoted properties when `keep_quoted` is set). Make sure we add this // check so we don't collide with a quoted name. do { mangled = base54(++cache.cname); - } while (!can_mangle(mangled) || (ignore_quoted && mangled in ignored)); + } while (!can_mangle(mangled) || keep_quoted && mangled in to_keep); } cache.props.set(name, mangled); @@ -219,7 +218,7 @@ function mangle_properties(ast, options) { return mangled; } - function addStrings(node, ignore) { + function addStrings(node, keep) { var out = {}; try { (function walk(node){ @@ -229,7 +228,7 @@ function mangle_properties(ast, options) { return true; } if (node instanceof AST_String) { - add(node.value, ignore); + add(node.value, keep); return true; } if (node instanceof AST_Conditional) { @@ -261,5 +260,4 @@ function mangle_properties(ast, options) { return node; })); } - } diff --git a/lib/scope.js b/lib/scope.js index 74760e4f..2ffca25a 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -76,7 +76,7 @@ SymbolDef.prototype = { else if (!this.mangled_name && !this.unmangleable(options)) { var s = this.scope; var sym = this.orig[0]; - if (!options.screw_ie8 && sym instanceof AST_SymbolLambda) + if (options.ie8 && sym instanceof AST_SymbolLambda) s = s.parent_scope; var def; if (this.defun && (def = this.defun.variables.get(this.name))) { @@ -93,7 +93,7 @@ SymbolDef.prototype = { AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ options = defaults(options, { cache: null, - screw_ie8: true, + ie8: false, }); // pass 1: setup scope chaining and handle definitions @@ -220,7 +220,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ self.walk(tw); // pass 3: fix up any scoping issue with IE8 - if (!options.screw_ie8) { + if (options.ie8) { self.walk(new TreeWalker(function(node, descend) { if (node instanceof AST_SymbolCatch) { var name = node.name; @@ -325,8 +325,8 @@ AST_Scope.DEFMETHOD("next_mangled", function(options){ if (!is_identifier(m)) continue; // skip over "do" // https://github.com/mishoo/UglifyJS2/issues/242 -- do not - // shadow a name excepted from mangling. - if (options.except.indexOf(m) >= 0) continue; + // shadow a name reserved from mangling. + if (options.reserved.indexOf(m) >= 0) continue; // we must ensure that the mangled name does not shadow a name // from some parent scope that is referenced in this or in @@ -399,10 +399,9 @@ AST_Symbol.DEFMETHOD("global", function(){ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { eval : false, - except : [], + ie8 : false, keep_fnames : false, - screw_ie8 : true, - sort : false, // Ignored. Flag retained for backwards compatibility. + reserved : [], toplevel : false, }); }); @@ -411,7 +410,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ options = this._default_mangler_options(options); // Never mangle arguments - options.except.push('arguments'); + options.reserved.push('arguments'); // We only need to mangle declaration nodes. Special logic wired // into the code generator will display the mangled name if it's @@ -422,7 +421,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ if (options.cache) { this.globals.each(function(symbol){ - if (options.except.indexOf(symbol.name) < 0) { + if (options.reserved.indexOf(symbol.name) < 0) { to_mangle.push(symbol); } }); @@ -439,7 +438,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ if (node instanceof AST_Scope) { var p = tw.parent(), a = []; node.variables.each(function(symbol){ - if (options.except.indexOf(symbol.name) < 0) { + if (options.reserved.indexOf(symbol.name) < 0) { a.push(symbol); } }); @@ -452,7 +451,7 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ node.mangled_name = name; return true; } - if (options.screw_ie8 && node instanceof AST_SymbolCatch) { + if (!options.ie8 && node instanceof AST_SymbolCatch) { to_mangle.push(node.definition()); return; } @@ -573,89 +572,3 @@ var base54 = (function() { }; return base54; })(); - -AST_Toplevel.DEFMETHOD("scope_warnings", function(options){ - options = defaults(options, { - assign_to_global : true, - eval : true, - func_arguments : true, - nested_defuns : true, - undeclared : false, // this makes a lot of noise - unreferenced : true, - }); - var tw = new TreeWalker(function(node){ - if (options.undeclared - && node instanceof AST_SymbolRef - && node.undeclared()) - { - // XXX: this also warns about JS standard names, - // i.e. Object, Array, parseInt etc. Should add a list of - // exceptions. - AST_Node.warn("Undeclared symbol: {name} [{file}:{line},{col}]", { - name: node.name, - file: node.start.file, - line: node.start.line, - col: node.start.col - }); - } - if (options.assign_to_global) - { - var sym = null; - if (node instanceof AST_Assign && node.left instanceof AST_SymbolRef) - sym = node.left; - else if (node instanceof AST_ForIn && node.init instanceof AST_SymbolRef) - sym = node.init; - if (sym - && (sym.undeclared() - || (sym.global() && sym.scope !== sym.definition().scope))) { - AST_Node.warn("{msg}: {name} [{file}:{line},{col}]", { - msg: sym.undeclared() ? "Accidental global?" : "Assignment to global", - name: sym.name, - file: sym.start.file, - line: sym.start.line, - col: sym.start.col - }); - } - } - if (options.eval - && node instanceof AST_SymbolRef - && node.undeclared() - && node.name == "eval") { - AST_Node.warn("Eval is used [{file}:{line},{col}]", node.start); - } - if (options.unreferenced - && (node instanceof AST_SymbolDeclaration || node instanceof AST_Label) - && !(node instanceof AST_SymbolCatch) - && node.unreferenced()) { - AST_Node.warn("{type} {name} is declared but not referenced [{file}:{line},{col}]", { - type: node instanceof AST_Label ? "Label" : "Symbol", - name: node.name, - file: node.start.file, - line: node.start.line, - col: node.start.col - }); - } - if (options.func_arguments - && node instanceof AST_Lambda - && node.uses_arguments) { - AST_Node.warn("arguments used in function {name} [{file}:{line},{col}]", { - name: node.name ? node.name.name : "anonymous", - file: node.start.file, - line: node.start.line, - col: node.start.col - }); - } - if (options.nested_defuns - && node instanceof AST_Defun - && !(tw.parent() instanceof AST_Scope)) { - AST_Node.warn("Function {name} declared in nested statement \"{type}\" [{file}:{line},{col}]", { - name: node.name.name, - type: tw.parent().TYPE, - file: node.start.file, - line: node.start.line, - col: node.start.col - }); - } - }); - this.walk(tw); -}); diff --git a/package.json b/package.json index cfa8eb74..45b3af9f 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "LICENSE" ], "dependencies": { - "source-map": "~0.5.1", - "yargs": "~3.10.0" + "commander": "~2.9.0", + "source-map": "~0.5.1" }, "devDependencies": { "acorn": "~0.6.0", diff --git a/test/benchmark.js b/test/benchmark.js index c150e5cf..c67a8548 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -7,7 +7,7 @@ var createHash = require("crypto").createHash; var fork = require("child_process").fork; var args = process.argv.slice(2); if (!args.length) { - args.push("-mc", "warnings=false"); + args.push("-mc"); } args.push("--stats"); var urls = [ @@ -29,11 +29,11 @@ function done() { var info = results[url]; console.log(); console.log(url); - console.log(info.log); var elapsed = 0; - info.log.replace(/: ([0-9]+\.[0-9]{3})s/g, function(match, time) { - elapsed += parseFloat(time); - }); + console.log(info.log.replace(/Elapsed: ([0-9]+)\s*/g, function(match, time) { + elapsed += 1e-3 * parseInt(time); + return ""; + })); console.log("Run-time:", elapsed.toFixed(3), "s"); console.log("Original:", info.input, "bytes"); console.log("Uglified:", info.output, "bytes"); diff --git a/test/compress/ascii.js b/test/compress/ascii.js index 2232d263..9662d413 100644 --- a/test/compress/ascii.js +++ b/test/compress/ascii.js @@ -2,7 +2,7 @@ ascii_only_true: { options = {} beautify = { ascii_only : true, - screw_ie8 : true, + ie8 : false, beautify : false, } input: { @@ -20,7 +20,7 @@ ascii_only_false: { options = {} beautify = { ascii_only : false, - screw_ie8 : true, + ie8 : false, beautify : false, } input: { @@ -33,4 +33,3 @@ ascii_only_false: { } expect_exact: 'function f(){return"\\x000\\x001\\x007\\08\\0"+"\\0\x01\x02\x03\x04\x05\x06\x07\\b\\t\\n\\v\\f\\r\x0e\x0f"+"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"+\' !"# ... }~\x7f\x80\x81 ... \xfe\xff\u0fff\uffff\'}' } - diff --git a/test/compress/issue-1321.js b/test/compress/issue-1321.js index 7449d3e2..dcbfde64 100644 --- a/test/compress/issue-1321.js +++ b/test/compress/issue-1321.js @@ -1,6 +1,6 @@ issue_1321_no_debug: { mangle_props = { - ignore_quoted: true + keep_quoted: true } input: { var x = {}; @@ -19,7 +19,7 @@ issue_1321_no_debug: { issue_1321_debug: { mangle_props = { - ignore_quoted: true, + keep_quoted: true, debug: "" } input: { @@ -39,7 +39,7 @@ issue_1321_debug: { issue_1321_with_quoted: { mangle_props = { - ignore_quoted: false + keep_quoted: false } input: { var x = {}; diff --git a/test/compress/issue-1446.js b/test/compress/issue-1446.js index 3d69aa09..cad1ae57 100644 --- a/test/compress/issue-1446.js +++ b/test/compress/issue-1446.js @@ -23,7 +23,7 @@ typeof_eq_undefined: { typeof_eq_undefined_ie8: { options = { comparisons: true, - screw_ie8: false + ie8: true, } input: { var a = typeof b != "undefined"; diff --git a/test/compress/issue-1588.js b/test/compress/issue-1588.js index fce9ba54..4e20a21d 100644 --- a/test/compress/issue-1588.js +++ b/test/compress/issue-1588.js @@ -1,9 +1,9 @@ screw_ie8: { options = { - screw_ie8: true, + ie8: false, } mangle = { - screw_ie8: true, + ie8: false, } input: { try { throw "foo"; } catch (x) { console.log(x); } @@ -16,10 +16,10 @@ screw_ie8: { support_ie8: { options = { - screw_ie8: false, + ie8: true, } mangle = { - screw_ie8: false, + ie8: true, } input: { try { throw "foo"; } catch (x) { console.log(x); } diff --git a/test/compress/issue-1704.js b/test/compress/issue-1704.js index a73f7f99..25e49522 100644 --- a/test/compress/issue-1704.js +++ b/test/compress/issue-1704.js @@ -1,10 +1,10 @@ mangle_catch: { options = { - screw_ie8: true, + ie8: false, toplevel: false, } mangle = { - screw_ie8: true, + ie8: false, toplevel: false, } input: { @@ -22,11 +22,11 @@ mangle_catch: { mangle_catch_ie8: { options = { - screw_ie8: false, + ie8: true, toplevel: false, } mangle = { - screw_ie8: false, + ie8: true, toplevel: false, } input: { @@ -44,11 +44,11 @@ mangle_catch_ie8: { mangle_catch_var: { options = { - screw_ie8: true, + ie8: false, toplevel: false, } mangle = { - screw_ie8: true, + ie8: false, toplevel: false, } input: { @@ -66,11 +66,11 @@ mangle_catch_var: { mangle_catch_var_ie8: { options = { - screw_ie8: false, + ie8: true, toplevel: false, } mangle = { - screw_ie8: false, + ie8: true, toplevel: false, } input: { @@ -88,11 +88,11 @@ mangle_catch_var_ie8: { mangle_catch_toplevel: { options = { - screw_ie8: true, + ie8: false, toplevel: true, } mangle = { - screw_ie8: true, + ie8: false, toplevel: true, } input: { @@ -110,11 +110,11 @@ mangle_catch_toplevel: { mangle_catch_ie8_toplevel: { options = { - screw_ie8: false, + ie8: true, toplevel: true, } mangle = { - screw_ie8: false, + ie8: true, toplevel: true, } input: { @@ -132,11 +132,11 @@ mangle_catch_ie8_toplevel: { mangle_catch_var_toplevel: { options = { - screw_ie8: true, + ie8: false, toplevel: true, } mangle = { - screw_ie8: true, + ie8: false, toplevel: true, } input: { @@ -154,11 +154,11 @@ mangle_catch_var_toplevel: { mangle_catch_var_ie8_toplevel: { options = { - screw_ie8: false, + ie8: true, toplevel: true, } mangle = { - screw_ie8: false, + ie8: true, toplevel: true, } input: { @@ -176,11 +176,11 @@ mangle_catch_var_ie8_toplevel: { mangle_catch_redef_1: { options = { - screw_ie8: true, + ie8: false, toplevel: false, } mangle = { - screw_ie8: true, + ie8: false, toplevel: false, } input: { @@ -198,11 +198,11 @@ mangle_catch_redef_1: { mangle_catch_redef_1_ie8: { options = { - screw_ie8: false, + ie8: true, toplevel: false, } mangle = { - screw_ie8: false, + ie8: true, toplevel: false, } input: { @@ -220,11 +220,11 @@ mangle_catch_redef_1_ie8: { mangle_catch_redef_1_toplevel: { options = { - screw_ie8: true, + ie8: false, toplevel: true, } mangle = { - screw_ie8: true, + ie8: false, toplevel: true, } input: { @@ -242,11 +242,11 @@ mangle_catch_redef_1_toplevel: { mangle_catch_redef_1_ie8_toplevel: { options = { - screw_ie8: false, + ie8: true, toplevel: true, } mangle = { - screw_ie8: false, + ie8: true, toplevel: true, } input: { @@ -264,11 +264,11 @@ mangle_catch_redef_1_ie8_toplevel: { mangle_catch_redef_2: { options = { - screw_ie8: true, + ie8: false, toplevel: false, } mangle = { - screw_ie8: true, + ie8: false, toplevel: false, } input: { @@ -285,11 +285,11 @@ mangle_catch_redef_2: { mangle_catch_redef_2_ie8: { options = { - screw_ie8: false, + ie8: true, toplevel: false, } mangle = { - screw_ie8: false, + ie8: true, toplevel: false, } input: { @@ -306,11 +306,11 @@ mangle_catch_redef_2_ie8: { mangle_catch_redef_2_toplevel: { options = { - screw_ie8: true, + ie8: false, toplevel: true, } mangle = { - screw_ie8: true, + ie8: false, toplevel: true, } input: { @@ -327,11 +327,11 @@ mangle_catch_redef_2_toplevel: { mangle_catch_redef_2_ie8_toplevel: { options = { - screw_ie8: false, + ie8: true, toplevel: true, } mangle = { - screw_ie8: false, + ie8: true, toplevel: true, } input: { diff --git a/test/compress/issue-1733.js b/test/compress/issue-1733.js index 3a940c96..f1e576c7 100644 --- a/test/compress/issue-1733.js +++ b/test/compress/issue-1733.js @@ -1,6 +1,6 @@ function_iife_catch: { mangle = { - screw_ie8: true, + ie8: false, } input: { function f(n) { @@ -21,7 +21,7 @@ function_iife_catch: { function_iife_catch_ie8: { mangle = { - screw_ie8: false, + ie8: true, } input: { function f(n) { @@ -42,7 +42,7 @@ function_iife_catch_ie8: { function_catch_catch: { mangle = { - screw_ie8: true, + ie8: false, } input: { var o = 0; @@ -70,7 +70,7 @@ function_catch_catch: { function_catch_catch_ie8: { mangle = { - screw_ie8: false, + ie8: true, } input: { var o = 0; diff --git a/test/compress/loops.js b/test/compress/loops.js index f13f5cc5..4d354bcf 100644 --- a/test/compress/loops.js +++ b/test/compress/loops.js @@ -245,7 +245,7 @@ issue_1532: { issue_186: { beautify = { beautify: false, - screw_ie8: true, + ie8: false, } input: { var x = 3; @@ -264,7 +264,7 @@ issue_186: { issue_186_ie8: { beautify = { beautify: false, - screw_ie8: false, + ie8: true, } input: { var x = 3; @@ -283,7 +283,7 @@ issue_186_ie8: { issue_186_beautify: { beautify = { beautify: true, - screw_ie8: true, + ie8: false, } input: { var x = 3; @@ -310,7 +310,7 @@ issue_186_beautify: { issue_186_beautify_ie8: { beautify = { beautify: true, - screw_ie8: false, + ie8: true, } input: { var x = 3; @@ -340,7 +340,7 @@ issue_186_bracketize: { beautify = { beautify: false, bracketize: true, - screw_ie8: true, + ie8: false, } input: { var x = 3; @@ -360,7 +360,7 @@ issue_186_bracketize_ie8: { beautify = { beautify: false, bracketize: true, - screw_ie8: false, + ie8: true, } input: { var x = 3; @@ -380,7 +380,7 @@ issue_186_beautify_bracketize: { beautify = { beautify: true, bracketize: true, - screw_ie8: true, + ie8: false, } input: { var x = 3; @@ -412,7 +412,7 @@ issue_186_beautify_bracketize_ie8: { beautify = { beautify: true, bracketize: true, - screw_ie8: false, + ie8: true, } input: { var x = 3; diff --git a/test/compress/properties.js b/test/compress/properties.js index 29bdfe2a..3e06dc1e 100644 --- a/test/compress/properties.js +++ b/test/compress/properties.js @@ -13,7 +13,7 @@ keep_properties: { dot_properties: { options = { properties: true, - screw_ie8: false + ie8: true, }; input: { a["foo"] = "bar"; @@ -36,7 +36,7 @@ dot_properties: { dot_properties_es5: { options = { properties: true, - screw_ie8: true + ie8: false, }; input: { a["foo"] = "bar"; @@ -125,7 +125,7 @@ evaluate_string_length: { mangle_properties: { mangle_props = { - ignore_quoted: false + keep_quoted: false }; input: { a["foo"] = "bar"; @@ -148,7 +148,7 @@ mangle_unquoted_properties: { properties: false } mangle_props = { - ignore_quoted: true + keep_quoted: true } beautify = { beautify: false, @@ -233,12 +233,12 @@ mangle_debug_suffix: { } } -mangle_debug_suffix_ignore_quoted: { +mangle_debug_suffix_keep_quoted: { options = { properties: false } mangle_props = { - ignore_quoted: true, + keep_quoted: true, debug: "XYZ", reserved: [] } diff --git a/test/compress/screw-ie8.js b/test/compress/screw-ie8.js index 68d1a364..a9fbeb51 100644 --- a/test/compress/screw-ie8.js +++ b/test/compress/screw-ie8.js @@ -1,9 +1,9 @@ do_screw: { options = { - screw_ie8: true, + ie8: false, } beautify = { - screw_ie8: true, + ie8: false, ascii_only: true, } input: { @@ -14,10 +14,10 @@ do_screw: { dont_screw: { options = { - screw_ie8: false, + ie8: true, } beautify = { - screw_ie8: false, + ie8: true, ascii_only: true, } input: { @@ -28,7 +28,7 @@ dont_screw: { do_screw_constants: { options = { - screw_ie8: true, + ie8: false, } input: { f(undefined, Infinity); @@ -38,7 +38,7 @@ do_screw_constants: { dont_screw_constants: { options = { - screw_ie8: false, + ie8: true, } input: { f(undefined, Infinity); @@ -47,9 +47,15 @@ dont_screw_constants: { } do_screw_try_catch: { - options = { screw_ie8: true }; - mangle = { screw_ie8: true }; - beautify = { screw_ie8: true }; + options = { + ie8: false, + } + mangle = { + ie8: false, + } + beautify = { + ie8: false, + } input: { good = function(e){ return function(error){ @@ -75,9 +81,15 @@ do_screw_try_catch: { } dont_screw_try_catch: { - options = { screw_ie8: false }; - mangle = { screw_ie8: false }; - beautify = { screw_ie8: false }; + options = { + ie8: true, + } + mangle = { + ie8: true, + } + beautify = { + ie8: true, + } input: { bad = function(e){ return function(error){ @@ -103,9 +115,15 @@ dont_screw_try_catch: { } do_screw_try_catch_undefined: { - options = { screw_ie8: true }; - mangle = { screw_ie8: true }; - beautify = { screw_ie8: true }; + options = { + ie8: false, + } + mangle = { + ie8: false, + } + beautify = { + ie8: false, + } input: { function a(b){ try { @@ -132,9 +150,15 @@ do_screw_try_catch_undefined: { } dont_screw_try_catch_undefined: { - options = { screw_ie8: false }; - mangle = { screw_ie8: false }; - beautify = { screw_ie8: false }; + options = { + ie8: true, + } + mangle = { + ie8: true, + } + beautify = { + ie8: true, + } input: { function a(b){ try { @@ -164,11 +188,11 @@ reduce_vars: { options = { evaluate: true, reduce_vars: true, - screw_ie8: false, + ie8: true, unused: true, } mangle = { - screw_ie8: false, + ie8: true, } input: { function f() { @@ -196,10 +220,10 @@ reduce_vars: { issue_1586_1: { options = { - screw_ie8: false, + ie8: true, } mangle = { - screw_ie8: false, + ie8: true, } input: { function f() { @@ -215,10 +239,10 @@ issue_1586_1: { issue_1586_2: { options = { - screw_ie8: true, + ie8: false, } mangle = { - screw_ie8: true, + ie8: false, } input: { function f() { diff --git a/test/jetstream.js b/test/jetstream.js index a8195389..56da7ad8 100644 --- a/test/jetstream.js +++ b/test/jetstream.js @@ -12,7 +12,7 @@ if (typeof phantom == "undefined") { }); var args = process.argv.slice(2); if (!args.length) { - args.push("-mc", "warnings=false"); + args.push("-mc"); } args.push("--stats"); var child_process = require("child_process"); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index b956309a..7162c816 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -2,6 +2,10 @@ var assert = require("assert"); var exec = require("child_process").exec; var readFileSync = require("fs").readFileSync; +function read(path) { + return readFileSync(path, "utf8"); +} + describe("bin/uglifyjs", function () { var uglifyjscmd = '"' + process.argv[0] + '" bin/uglifyjs'; it("should produce a functional build when using --self", function (done) { @@ -20,7 +24,7 @@ describe("bin/uglifyjs", function () { done(); }); }); - it("Should be able to filter comments correctly with `--comment all`", function (done) { + it("Should be able to filter comments correctly with `--comments all`", function (done) { var command = uglifyjscmd + ' test/input/comments/filter.js --comments all'; exec(command, function (err, stdout) { @@ -50,8 +54,8 @@ describe("bin/uglifyjs", function () { done(); }); }); - it("Should append source map to output when using --source-map-inline", function (done) { - var command = uglifyjscmd + ' test/input/issue-1323/sample.js --source-map-inline'; + it("Should append source map to output when using --source-map url=inline", function (done) { + var command = uglifyjscmd + " test/input/issue-1323/sample.js --source-map url=inline"; exec(command, function (err, stdout) { if (err) throw err; @@ -61,7 +65,7 @@ describe("bin/uglifyjs", function () { done(); }); }); - it("should not append source map to output when not using --source-map-inline", function (done) { + it("should not append source map to output when not using --source-map url=inline", function (done) { var command = uglifyjscmd + ' test/input/issue-1323/sample.js'; exec(command, function (err, stdout) { @@ -137,7 +141,7 @@ describe("bin/uglifyjs", function () { exec(command, function (err, stdout) { if (err) throw err; - assert.strictEqual(stdout, readFileSync("test/input/issue-1482/default.js", "utf8")); + assert.strictEqual(stdout, read("test/input/issue-1482/default.js")); done(); }); }); @@ -147,55 +151,59 @@ describe("bin/uglifyjs", function () { exec(command, function (err, stdout) { if (err) throw err; - assert.strictEqual(stdout, readFileSync("test/input/issue-1482/bracketize.js", "utf8")); + assert.strictEqual(stdout, read("test/input/issue-1482/bracketize.js")); done(); }); }); it("Should process inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js -mc toplevel --in-source-map inline --source-map-inline'; + var command = uglifyjscmd + " test/input/issue-520/input.js -mc toplevel --source-map content=inline,url=inline"; exec(command, function (err, stdout) { if (err) throw err; - assert.strictEqual(stdout, readFileSync("test/input/issue-520/output.js", "utf8")); + assert.strictEqual(stdout, read("test/input/issue-520/output.js")); done(); }); }); it("Should warn for missing inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-1323/sample.js --in-source-map inline'; + var command = uglifyjscmd + " test/input/issue-1323/sample.js --source-map content=inline,url=inline"; exec(command, function (err, stdout, stderr) { if (err) throw err; - assert.strictEqual(stdout, "var bar=function(){function foo(bar){return bar}return foo}();\n"); + assert.strictEqual(stdout, [ + "var bar=function(){function foo(bar){return bar}return foo}();", + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3QvaW5wdXQvaXNzdWUtMTMyMy9zYW1wbGUuanMiXSwibmFtZXMiOlsiYmFyIiwiZm9vIl0sIm1hcHBpbmdzIjoiQUFBQSxHQUFJQSxLQUFNLFdBQ04sUUFBU0MsS0FBS0QsS0FDVixNQUFPQSxLQUdYLE1BQU9DIn0=", + "", + ].join("\n")); assert.strictEqual(stderr, "WARN: inline source map not found\n"); done(); }); }); it("Should fail with multiple input and inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js test/input/issue-520/output.js --in-source-map inline --source-map-inline'; + var command = uglifyjscmd + " test/input/issue-520/input.js test/input/issue-520/output.js --source-map content=inline,url=inline"; exec(command, function (err, stdout, stderr) { assert.ok(err); - assert.strictEqual(stderr, "ERROR: Inline source map only works with singular input\n"); + assert.strictEqual(stderr, "ERROR: inline source map only works with singular input\n"); done(); }); }); it("Should fail with acorn and inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js --in-source-map inline --source-map-inline --acorn'; + var command = uglifyjscmd + " test/input/issue-520/input.js --source-map content=inline,url=inline -p acorn"; exec(command, function (err, stdout, stderr) { assert.ok(err); - assert.strictEqual(stderr, "ERROR: Inline source map only works with built-in parser\n"); + assert.strictEqual(stderr, "ERROR: inline source map only works with built-in parser\n"); done(); }); }); it("Should fail with SpiderMonkey and inline source map", function(done) { - var command = uglifyjscmd + ' test/input/issue-520/input.js --in-source-map inline --source-map-inline --spidermonkey'; + var command = uglifyjscmd + " test/input/issue-520/input.js --source-map content=inline,url=inline -p spidermonkey"; exec(command, function (err, stdout, stderr) { assert.ok(err); - assert.strictEqual(stderr, "ERROR: Inline source map only works with built-in parser\n"); + assert.strictEqual(stderr, "ERROR: inline source map only works with built-in parser\n"); done(); }); }); @@ -208,7 +216,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(lines[0], "Parse error at test/input/invalid/simple.js:1,12"); assert.strictEqual(lines[1], "function f(a{}"); assert.strictEqual(lines[2], " ^"); - assert.strictEqual(lines[3], "SyntaxError: Unexpected token punc «{», expected punc «,»"); + assert.strictEqual(lines[3], "ERROR: Unexpected token punc «{», expected punc «,»"); done(); }); }); @@ -221,7 +229,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(lines[0], "Parse error at test/input/invalid/tab.js:1,12"); assert.strictEqual(lines[1], "\t\tfoo(\txyz, 0abc);"); assert.strictEqual(lines[2], "\t\t \t ^"); - assert.strictEqual(lines[3], "SyntaxError: Invalid syntax: 0abc"); + assert.strictEqual(lines[3], "ERROR: Invalid syntax: 0abc"); done(); }); }); @@ -234,7 +242,7 @@ describe("bin/uglifyjs", function () { assert.strictEqual(lines[0], "Parse error at test/input/invalid/eof.js:2,0"); assert.strictEqual(lines[1], "foo, bar("); assert.strictEqual(lines[2], " ^"); - assert.strictEqual(lines[3], "SyntaxError: Unexpected token: eof (undefined)"); + assert.strictEqual(lines[3], "ERROR: Unexpected token: eof (undefined)"); done(); }); }); @@ -247,20 +255,10 @@ describe("bin/uglifyjs", function () { assert.strictEqual(lines[0], "Parse error at test/input/invalid/loop-no-body.js:2,0"); assert.strictEqual(lines[1], "for (var i = 0; i < 1; i++) "); assert.strictEqual(lines[2], " ^"); - assert.strictEqual(lines[3], "SyntaxError: Unexpected token: eof (undefined)"); + assert.strictEqual(lines[3], "ERROR: Unexpected token: eof (undefined)"); done(); }); }); - it("Should support hyphen as shorthand", function(done) { - var command = uglifyjscmd + ' test/input/issue-1431/sample.js -m keep-fnames=true'; - - exec(command, function (err, stdout) { - if (err) throw err; - - assert.strictEqual(stdout, "function f(r){return function(){function n(n){return n*n}return r(n)}}function g(n){return n(1)+n(2)}console.log(f(g)()==5);\n"); - done(); - }); - }); it("Should throw syntax error (5--)", function(done) { var command = uglifyjscmd + ' test/input/invalid/assign_1.js'; @@ -271,7 +269,7 @@ describe("bin/uglifyjs", function () { "Parse error at test/input/invalid/assign_1.js:1,18", "console.log(1 || 5--);", " ^", - "SyntaxError: Invalid use of -- operator" + "ERROR: Invalid use of -- operator" ].join("\n")); done(); }); @@ -286,7 +284,7 @@ describe("bin/uglifyjs", function () { "Parse error at test/input/invalid/assign_2.js:1,32", "console.log(2 || (Math.random() /= 2));", " ^", - "SyntaxError: Invalid assignment" + "ERROR: Invalid assignment" ].join("\n")); done(); }); @@ -301,9 +299,34 @@ describe("bin/uglifyjs", function () { "Parse error at test/input/invalid/assign_3.js:1,18", "console.log(3 || ++this);", " ^", - "SyntaxError: Invalid use of ++ operator" + "ERROR: Invalid use of ++ operator" ].join("\n")); done(); }); }); + it("Should handle literal string as source map input", function(done) { + var command = [ + uglifyjscmd, + "test/input/issue-1236/simple.js", + "--source-map", + 'content="' + read_map() + '",url=inline' + ].join(" "); + + exec(command, function (err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, [ + '"use strict";var foo=function foo(x){return"foo "+x};console.log(foo("bar"));', + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sIm5hbWVzIjpbImZvbyIsIngiLCJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiWUFBQSxJQUFJQSxLQUFNLFFBQU5BLEtBQU1DLEdBQUEsTUFBSyxPQUFTQSxFQUN4QkMsU0FBUUMsSUFBSUgsSUFBSSJ9", + "" + ].join("\n")); + done(); + }); + + function read_map() { + var map = JSON.parse(read("./test/input/issue-1236/simple.js.map")); + delete map.sourcesContent; + return JSON.stringify(map).replace(/"/g, '\\"'); + } + }); }); diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js index ec17aa8c..4b74ebf9 100644 --- a/test/mocha/comment-filter.js +++ b/test/mocha/comment-filter.js @@ -75,7 +75,6 @@ describe("comment filters", function() { it("Should handle shebang and preamble correctly", function() { var code = UglifyJS.minify("#!/usr/bin/node\nvar x = 10;", { - fromString: true, output: { preamble: "/* Build */" } }).code; assert.strictEqual(code, "#!/usr/bin/node\n/* Build */\nvar x=10;"); @@ -83,7 +82,6 @@ describe("comment filters", function() { it("Should handle preamble without shebang correctly", function() { var code = UglifyJS.minify("var x = 10;", { - fromString: true, output: { preamble: "/* Build */" } }).code; assert.strictEqual(code, "/* Build */\nvar x=10;"); diff --git a/test/mocha/comment.js b/test/mocha/comment.js index 56470e0f..acad3693 100644 --- a/test/mocha/comment.js +++ b/test/mocha/comment.js @@ -20,7 +20,7 @@ describe("Comment", function() { for (var i = 0; i < tests.length; i++) { assert.throws(function() { - uglify.parse(tests[i], {fromString: true}) + uglify.parse(tests[i]); }, fail, tests[i]); } }); @@ -43,7 +43,7 @@ describe("Comment", function() { for (var i = 0; i < tests.length; i++) { assert.throws(function() { - uglify.parse(tests[i], {fromString: true}) + uglify.parse(tests[i]); }, fail, tests[i]); } }); diff --git a/test/mocha/comment_before_constant.js b/test/mocha/comment_before_constant.js index eaa8691c..9b69e078 100644 --- a/test/mocha/comment_before_constant.js +++ b/test/mocha/comment_before_constant.js @@ -6,9 +6,7 @@ describe("comment before constant", function() { it("Should test comment before constant is retained and output after mangle.", function() { var result = Uglify.minify(js, { - fromString: true, compress: { collapse_vars: false, reduce_vars: false }, - mangle: {}, output: { comments: true }, }); assert.strictEqual(result.code, 'function f(){/*c1*/var/*c2*/n=/*c3*/!1;return n}'); @@ -16,12 +14,9 @@ describe("comment before constant", function() { it("Should test code works when comments disabled.", function() { var result = Uglify.minify(js, { - fromString: true, compress: { collapse_vars: false, reduce_vars: false }, - mangle: {}, output: { comments: false }, }); assert.strictEqual(result.code, 'function f(){var n=!1;return n}'); }); }); - diff --git a/test/mocha/directives.js b/test/mocha/directives.js index 5189f1ad..16279a5d 100644 --- a/test/mocha/directives.js +++ b/test/mocha/directives.js @@ -197,7 +197,7 @@ describe("Directives", function() { assert.strictEqual( uglify.minify( '"use strict";\'use strict\';"use strict";"use strict";;\'use strict\';console.log(\'use strict\');', - {fromString: true, output: {beautify: true, quote_style: 3}, compress: false} + {output: {beautify: true, quote_style: 3}, compress: false} ).code, '"use strict";\n\n\'use strict\';\n\n"use strict";\n\n"use strict";\n\n;\'use strict\';\n\nconsole.log(\'use strict\');' ); @@ -225,7 +225,7 @@ describe("Directives", function() { for (var i = 0; i < tests.length; i++) { assert.strictEqual( - uglify.minify(tests[i][0], {fromString: true, quote_style: 3, compress: false, mangle: false}).code, + uglify.minify(tests[i][0], {compress: false, mangle: false}).code, tests[i][1], tests[i][0] ); @@ -234,7 +234,7 @@ describe("Directives", function() { it("Should add double semicolon when relying on automatic semicolon insertion", function() { var code = uglify.minify('"use strict";"use\\x20strict";', - {fromString: true, output: {semicolons: false}, compress: false} + {output: {semicolons: false}, compress: false} ).code; assert.strictEqual(code, '"use strict";;"use strict"\n'); }); @@ -340,7 +340,7 @@ describe("Directives", function() { ]; for (var i = 0; i < tests.length; i++) { assert.strictEqual( - uglify.minify(tests[i][0], {fromString: true, output:{quote_style: tests[i][1]}, compress: false}).code, + uglify.minify(tests[i][0], {output:{quote_style: tests[i][1]}, compress: false}).code, tests[i][2], tests[i][0] + " using mode " + tests[i][1] ); @@ -362,7 +362,7 @@ describe("Directives", function() { for (var i = 0; i < tests.length; i++) { assert.strictEqual( - uglify.minify(tests[i][0], {fromString: true, compress: {collapse_vars: true, side_effects: true}}).code, + uglify.minify(tests[i][0], {compress: {collapse_vars: true, side_effects: true}}).code, tests[i][1], tests[i][0] ); diff --git a/test/mocha/glob.js b/test/mocha/glob.js index e9555a52..56d3f82a 100644 --- a/test/mocha/glob.js +++ b/test/mocha/glob.js @@ -1,58 +1,80 @@ -var Uglify = require('../../'); var assert = require("assert"); +var exec = require("child_process").exec; var path = require("path"); +var readFileSync = require("fs").readFileSync; -describe("minify() with input file globs", function() { - it("minify() with one input file glob string.", function() { - var result = Uglify.minify("test/input/issue-1242/foo.*"); - assert.strictEqual(result.code, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);'); - }); - it("minify() with an array of one input file glob.", function() { - var result = Uglify.minify([ - "test/input/issue-1242/b*.es5", - ]); - assert.strictEqual(result.code, 'function bar(n){return 3*n}function baz(n){return n/2}'); - }); - it("minify() with an array of multiple input file globs.", function() { - var result = Uglify.minify([ - "test/input/issue-1242/???.es5", - "test/input/issue-1242/*.js", - ], { - compress: { toplevel: true } +describe("bin/uglifyjs with input file globs", function() { + var uglifyjscmd = '"' + process.argv[0] + '" bin/uglifyjs'; + it("bin/uglifyjs with one input file extension glob.", function(done) { + var command = uglifyjscmd + ' "test/input/issue-1242/foo.*" -cm'; + + exec(command, function(err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, 'function foo(o){print("Foo:",2*o)}var print=console.log.bind(console);\n'); + done(); }); - assert.strictEqual(result.code, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){print("Foo:",2*n)}(11);'); }); - it("should throw with non-matching glob string", function() { - var glob = "test/input/issue-1242/blah.*"; - assert.strictEqual(Uglify.simple_glob(glob).length, 1); - assert.strictEqual(Uglify.simple_glob(glob)[0], glob); - assert.throws(function() { - Uglify.minify(glob); - }, "should throw file not found"); + it("bin/uglifyjs with one input file name glob.", function(done) { + var command = uglifyjscmd + ' "test/input/issue-1242/b*.es5" -cm'; + + exec(command, function(err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, 'function bar(n){return 3*n}function baz(n){return n/2}\n'); + done(); + }); }); - it('"?" in glob string should not match "/"', function() { - var glob = "test/input?issue-1242/foo.*"; - assert.strictEqual(Uglify.simple_glob(glob).length, 1); - assert.strictEqual(Uglify.simple_glob(glob)[0], glob); - assert.throws(function() { - Uglify.minify(glob); - }, "should throw file not found"); + it("bin/uglifyjs with multiple input file globs.", function(done) { + var command = uglifyjscmd + ' "test/input/issue-1242/???.es5" "test/input/issue-1242/*.js" -mc toplevel'; + + exec(command, function(err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, 'var print=console.log.bind(console),a=function(n){return 3*n}(3),b=function(n){return n/2}(12);print("qux",a,b),function(n){print("Foo:",2*n)}(11);\n'); + done(); + }); }); - it("should handle special characters in glob string", function() { - var result = Uglify.minify("test/input/issue-1632/^{*}[???](*)+$.??"); - assert.strictEqual(result.code, "console.log(x);"); + it("should throw with non-matching glob string", function(done) { + var command = uglifyjscmd + ' "test/input/issue-1242/blah.*"'; + + exec(command, function(err, stdout, stderr) { + assert.ok(err); + assert.ok(/^ERROR: ENOENT/.test(stderr)); + done(); + }); }); - it("should handle array of glob strings - matching and otherwise", function() { + it('"?" in glob string should not match "/"', function(done) { + var command = uglifyjscmd + ' "test/input?issue-1242/foo.*"'; + + exec(command, function(err, stdout, stderr) { + assert.ok(err); + assert.ok(/^ERROR: ENOENT/.test(stderr)); + done(); + }); + }); + it("should handle special characters in glob string", function(done) { + var command = uglifyjscmd + ' "test/input/issue-1632/^{*}[???](*)+$.??" -cm'; + + exec(command, function(err, stdout) { + if (err) throw err; + + assert.strictEqual(stdout, "console.log(x);\n"); + done(); + }); + }); + it("should handle array of glob strings - matching and otherwise", function(done) { var dir = "test/input/issue-1242"; - var matches = Uglify.simple_glob([ + var command = uglifyjscmd + ' "' + [ path.join(dir, "b*.es5"), path.join(dir, "z*.es5"), - path.join(dir, "*.js"), - ]); - assert.strictEqual(matches.length, 4); - assert.strictEqual(matches[0], path.join(dir, "bar.es5")); - assert.strictEqual(matches[1], path.join(dir, "baz.es5")); - assert.strictEqual(matches[2], path.join(dir, "z*.es5")); - assert.strictEqual(matches[3], path.join(dir, "qux.js")); + path.join(dir, "*.js") + ].join('" "') + '"'; + + exec(command, function(err, stdout, stderr) { + assert.ok(err); + assert.ok(/^ERROR: ENOENT.*?z\*\.es5/.test(stderr)); + done(); + }); }); }); diff --git a/test/mocha/huge-number-of-comments.js b/test/mocha/huge-number-of-comments.js index 3b90bc0e..a58f8d0a 100644 --- a/test/mocha/huge-number-of-comments.js +++ b/test/mocha/huge-number-of-comments.js @@ -8,12 +8,7 @@ describe("Huge number of comments.", function() { for (i = 1; i <= 5000; ++i) { js += "// " + i + "\n"; } for (; i <= 10000; ++i) { js += "/* " + i + " */ /**/"; } js += "x; }"; - var result = Uglify.minify(js, { - fromString: true, - mangle: false, - compress: {} - }); + var result = Uglify.minify(js, { mangle: false }); assert.strictEqual(result.code, "function lots_of_comments(x){return 7-x}"); }); }); - diff --git a/test/mocha/input-sourcemaps.js b/test/mocha/input-sourcemaps.js index d5284e3c..bda6e1a2 100644 --- a/test/mocha/input-sourcemaps.js +++ b/test/mocha/input-sourcemaps.js @@ -25,9 +25,9 @@ describe("input sourcemaps", function() { transpilemap = sourceMap || getMap(); var result = Uglify.minify(transpiled, { - fromString: true, - inSourceMap: transpilemap, - outSourceMap: true + sourceMap: { + content: transpilemap + } }); map = new SourceMapConsumer(result.map); diff --git a/test/mocha/let.js b/test/mocha/let.js index 89fd9f1a..f41fd59b 100644 --- a/test/mocha/let.js +++ b/test/mocha/let.js @@ -11,7 +11,7 @@ describe("let", function() { s += "var v" + i + "=0;"; } s += '}'; - var result = Uglify.minify(s, {fromString: true, compress: false}); + var result = Uglify.minify(s, {compress: false}); // Verify that select keywords and reserved keywords not produced assert.strictEqual(result.code.indexOf("var let="), -1); diff --git a/test/mocha/line-endings.js b/test/mocha/line-endings.js index 10e2a1c5..379ee2b9 100644 --- a/test/mocha/line-endings.js +++ b/test/mocha/line-endings.js @@ -3,9 +3,8 @@ var assert = require("assert"); describe("line-endings", function() { var options = { - fromString: true, - mangle: false, compress: false, + mangle: false, output: { beautify: false, comments: /^!/, diff --git a/test/mocha/minify-file-map.js b/test/mocha/minify-file-map.js index 169e730e..cae5ccb7 100644 --- a/test/mocha/minify-file-map.js +++ b/test/mocha/minify-file-map.js @@ -6,43 +6,41 @@ describe("Input file as map", function() { var jsMap = { '/scripts/foo.js': 'var foo = {"x": 1, y: 2, \'z\': 3};' }; - var result = Uglify.minify(jsMap, {fromString: true, outSourceMap: true}); + var result = Uglify.minify(jsMap, {sourceMap: true}); var map = JSON.parse(result.map); assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3};'); assert.deepEqual(map.sources, ['/scripts/foo.js']); assert.strictEqual(map.file, undefined); - result = Uglify.minify(jsMap, {fromString: true, outFileName: 'out.js'}); - assert.strictEqual(result.map, null); + result = Uglify.minify(jsMap); + assert.strictEqual(result.map, undefined); - result = Uglify.minify(jsMap, {fromString: true, outFileName: 'out.js', outSourceMap: true}); + result = Uglify.minify(jsMap, {sourceMap: {filename: 'out.js'}}); map = JSON.parse(result.map); assert.strictEqual(map.file, 'out.js'); }); - it("Should accept array of objects and strings", function() { + it("Should accept array of strings", function() { var jsSeq = [ - {'/scripts/foo.js': 'var foo = {"x": 1, y: 2, \'z\': 3};'}, + 'var foo = {"x": 1, y: 2, \'z\': 3};', 'var bar = 15;' ]; - var result = Uglify.minify(jsSeq, {fromString: true, outSourceMap: true}); + var result = Uglify.minify(jsSeq, {sourceMap: true}); var map = JSON.parse(result.map); assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3},bar=15;'); - assert.strictEqual(map.sources[0], '/scripts/foo.js'); + assert.deepEqual(map.sources, ['0', '1']); }); it("Should correctly include source", function() { - var jsSeq = [ - {'/scripts/foo.js': 'var foo = {"x": 1, y: 2, \'z\': 3};'}, - 'var bar = 15;' - ]; - var result = Uglify.minify(jsSeq, {fromString: true, outSourceMap: true, sourceMapIncludeSources: true}); + var jsMap = { + '/scripts/foo.js': 'var foo = {"x": 1, y: 2, \'z\': 3};' + }; + var result = Uglify.minify(jsMap, {sourceMap: {includeSources: true}}); var map = JSON.parse(result.map); - assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3},bar=15;'); - assert.deepEqual(map.sourcesContent, ['var foo = {"x": 1, y: 2, \'z\': 3};', 'var bar = 15;']); + assert.strictEqual(result.code, 'var foo={x:1,y:2,z:3};'); + assert.deepEqual(map.sourcesContent, ['var foo = {"x": 1, y: 2, \'z\': 3};']); }); - }); diff --git a/test/mocha/minify.js b/test/mocha/minify.js index a4587cb7..18840a58 100644 --- a/test/mocha/minify.js +++ b/test/mocha/minify.js @@ -2,10 +2,14 @@ var Uglify = require('../../'); var assert = require("assert"); var readFileSync = require("fs").readFileSync; +function read(path) { + return readFileSync(path, "utf8"); +} + describe("minify", function() { it("Should test basic sanity of minify with default options", function() { var js = 'function foo(bar) { if (bar) return 3; else return 7; var u = not_called(); }'; - var result = Uglify.minify(js, {fromString: true}); + var result = Uglify.minify(js); assert.strictEqual(result.code, 'function foo(n){return n?3:7}'); }); @@ -13,7 +17,7 @@ describe("minify", function() { it("Should preserve quotes in object literals", function() { var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; var result = Uglify.minify(js, { - fromString: true, output: { + output: { keep_quoted_props: true }}); assert.strictEqual(result.code, 'var foo={"x":1,y:2,"z":3};'); @@ -22,7 +26,7 @@ describe("minify", function() { it("Should preserve quote styles when quote_style is 3", function() { var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; var result = Uglify.minify(js, { - fromString: true, output: { + output: { keep_quoted_props: true, quote_style: 3 }}); @@ -32,7 +36,7 @@ describe("minify", function() { it("Should not preserve quotes in object literals when disabled", function() { var js = 'var foo = {"x": 1, y: 2, \'z\': 3};'; var result = Uglify.minify(js, { - fromString: true, output: { + output: { keep_quoted_props: false, quote_style: 3 }}); @@ -44,12 +48,13 @@ describe("minify", function() { it("Shouldn't mangle quoted properties", function() { var js = 'a["foo"] = "bar"; a.color = "red"; x = {"bar": 10};'; var result = Uglify.minify(js, { - fromString: true, compress: { properties: false }, - mangleProperties: { - ignore_quoted: true + mangle: { + properties: { + keep_quoted: true + } }, output: { keep_quoted_props: true, @@ -63,10 +68,12 @@ describe("minify", function() { describe("inSourceMap", function() { it("Should read the given string filename correctly when sourceMapIncludeSources is enabled (#1236)", function() { - var result = Uglify.minify('./test/input/issue-1236/simple.js', { - outSourceMap: "simple.min.js.map", - inSourceMap: "./test/input/issue-1236/simple.js.map", - sourceMapIncludeSources: true + var result = Uglify.minify(read("./test/input/issue-1236/simple.js"), { + sourceMap: { + content: read("./test/input/issue-1236/simple.js.map"), + filename: "simple.min.js", + includeSources: true + } }); var map = JSON.parse(result.map); @@ -77,10 +84,12 @@ describe("minify", function() { 'let foo = x => "foo " + x;\nconsole.log(foo("bar"));'); }); it("Should process inline source map", function() { - var code = Uglify.minify("./test/input/issue-520/input.js", { + var code = Uglify.minify(read("./test/input/issue-520/input.js"), { compress: { toplevel: true }, - inSourceMap: "inline", - sourceMapInline: true + sourceMap: { + content: "inline", + url: "inline" + } }).code + "\n"; assert.strictEqual(code, readFileSync("test/input/issue-520/output.js", "utf8")); }); @@ -91,9 +100,11 @@ describe("minify", function() { warnings.push(txt); }; try { - var result = Uglify.minify("./test/input/issue-1323/sample.js", { - inSourceMap: "inline", + var result = Uglify.minify(read("./test/input/issue-1323/sample.js"), { mangle: false, + sourceMap: { + content: "inline" + } }); assert.strictEqual(result.code, "var bar=function(){function foo(bar){return bar}return foo}();"); assert.strictEqual(warnings.length, 1); @@ -105,20 +116,13 @@ describe("minify", function() { it("Should fail with multiple input and inline source map", function() { assert.throws(function() { Uglify.minify([ - "./test/input/issue-520/input.js", - "./test/input/issue-520/output.js" + read("./test/input/issue-520/input.js"), + read("./test/input/issue-520/output.js") ], { - inSourceMap: "inline", - sourceMapInline: true - }); - }); - }); - it("Should fail with SpiderMonkey and inline source map", function() { - assert.throws(function() { - Uglify.minify("./test/input/issue-520/input.js", { - inSourceMap: "inline", - sourceMapInline: true, - spidermonkey: true + sourceMap: { + content: "inline", + url: "inline" + } }); }); }); @@ -127,17 +131,16 @@ describe("minify", function() { describe("sourceMapInline", function() { it("should append source map to output js when sourceMapInline is enabled", function() { var result = Uglify.minify('var a = function(foo) { return foo; };', { - fromString: true, - sourceMapInline: true + sourceMap: { + url: "inline" + } }); var code = result.code; assert.strictEqual(code, "var a=function(n){return n};\n" + - "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIj8iXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsR0FBSUEsR0FBSSxTQUFTQyxHQUFPLE1BQU9BIn0="); + "//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIjAiXSwibmFtZXMiOlsiYSIsImZvbyJdLCJtYXBwaW5ncyI6IkFBQUEsR0FBSUEsR0FBSSxTQUFTQyxHQUFPLE1BQU9BIn0="); }); it("should not append source map to output js when sourceMapInline is not enabled", function() { - var result = Uglify.minify('var a = function(foo) { return foo; };', { - fromString: true - }); + var result = Uglify.minify('var a = function(foo) { return foo; };'); var code = result.code; assert.strictEqual(code, "var a=function(n){return n};"); }); @@ -146,7 +149,6 @@ describe("minify", function() { describe("#__PURE__", function() { it("should drop #__PURE__ hint after use", function() { var result = Uglify.minify('//@__PURE__ comment1 #__PURE__ comment2\n foo(), bar();', { - fromString: true, output: { comments: "all", beautify: false, @@ -157,7 +159,6 @@ describe("minify", function() { }); it("should not drop #__PURE__ hint if function is retained", function() { var result = Uglify.minify("var a = /*#__PURE__*/(function(){ foo(); })();", { - fromString: true, output: { comments: "all", beautify: false, @@ -171,11 +172,11 @@ describe("minify", function() { describe("JS_Parse_Error", function() { it("should throw syntax error", function() { assert.throws(function() { - Uglify.minify("function f(a{}", { fromString: true }); + Uglify.minify("function f(a{}"); }, function(err) { assert.ok(err instanceof Error); assert.strictEqual(err.stack.split(/\n/)[0], "SyntaxError: Unexpected token punc «{», expected punc «,»"); - assert.strictEqual(err.filename, 0); + assert.strictEqual(err.filename, "0"); assert.strictEqual(err.line, 1); assert.strictEqual(err.col, 12); return true; @@ -191,5 +192,4 @@ describe("minify", function() { assert.strictEqual(ast.print_to_string(), "function f(a){for(var i=0;i 1) { - throw new Error("inline source map only works with singular input"); - } - } - [].concat(files).forEach(function (files, i) { - if (typeof files === 'string') { - addFile(files, options.fromString ? i : files); - } else { - for (var fileUrl in files) { - addFile(files[fileUrl], fileUrl); - } - } - }); - } - if (options.wrap) { - toplevel = toplevel.wrap_commonjs(options.wrap, options.exportAll); - } - - // 2. compress - if (options.compress) { - var compress = { warnings: options.warnings }; - UglifyJS.merge(compress, options.compress); - toplevel.figure_out_scope(options.mangle); - var sq = UglifyJS.Compressor(compress); - toplevel = sq.compress(toplevel); - } - - // 3. mangle properties - if (options.mangleProperties || options.nameCache) { - options.mangleProperties.cache = UglifyJS.readNameCache(options.nameCache, "props"); - toplevel = UglifyJS.mangle_properties(toplevel, options.mangleProperties); - UglifyJS.writeNameCache(options.nameCache, "props", options.mangleProperties.cache); - } - - // 4. mangle - if (options.mangle) { - toplevel.figure_out_scope(options.mangle); - toplevel.compute_char_frequency(options.mangle); - toplevel.mangle_names(options.mangle); - } - - // 5. output - var output = { max_line_len: 32000 }; - if (options.outSourceMap || options.sourceMapInline) { - output.source_map = UglifyJS.SourceMap({ - // prefer outFileName, otherwise use outSourceMap without .map suffix - file: options.outFileName || (typeof options.outSourceMap === 'string' ? options.outSourceMap.replace(/\.map$/i, '') : null), - orig: inMap, - root: options.sourceRoot - }); - if (options.sourceMapIncludeSources) { - for (var file in sourcesContent) { - if (sourcesContent.hasOwnProperty(file)) { - output.source_map.get().setSourceContent(file, sourcesContent[file]); - } - } - } - - } - if (options.output) { - UglifyJS.merge(output, options.output); - } - var stream = UglifyJS.OutputStream(output); - toplevel.print(stream); - - - var source_map = output.source_map; - if (source_map) { - source_map = source_map + ""; - } - - var mappingUrlPrefix = "\n//# sourceMappingURL="; - if (options.sourceMapInline) { - stream += mappingUrlPrefix + "data:application/json;charset=utf-8;base64," + new Buffer(source_map).toString("base64"); - } else if (options.outSourceMap && typeof options.outSourceMap === "string" && options.sourceMapUrl !== false) { - stream += mappingUrlPrefix + (typeof options.sourceMapUrl === "string" ? options.sourceMapUrl : options.outSourceMap); - } - - return { - code : stream + "", - map : source_map - }; -}; - -// UglifyJS.describe_ast = function() { -// function doitem(ctor) { -// var sub = {}; -// ctor.SUBCLASSES.forEach(function(ctor){ -// sub[ctor.TYPE] = doitem(ctor); -// }); -// var ret = {}; -// if (ctor.SELF_PROPS.length > 0) ret.props = ctor.SELF_PROPS; -// if (ctor.SUBCLASSES.length > 0) ret.sub = sub; -// return ret; -// } -// return doitem(UglifyJS.AST_Node).sub; -// } - UglifyJS.describe_ast = function() { var out = UglifyJS.OutputStream({ beautify: true }); function doitem(ctor) { @@ -227,94 +59,3 @@ UglifyJS.describe_ast = function() { doitem(UglifyJS.AST_Node); return out + ""; }; - -function readReservedFile(filename, reserved) { - if (!reserved) { - reserved = { vars: [], props: [] }; - } - var data = fs.readFileSync(filename, "utf8"); - data = JSON.parse(data); - if (data.vars) { - data.vars.forEach(function(name){ - UglifyJS.push_uniq(reserved.vars, name); - }); - } - if (data.props) { - data.props.forEach(function(name){ - UglifyJS.push_uniq(reserved.props, name); - }); - } - return reserved; -} - -UglifyJS.readReservedFile = readReservedFile; - -UglifyJS.readDefaultReservedFile = function(reserved) { - return readReservedFile(require.resolve("./domprops.json"), reserved); -}; - -UglifyJS.readNameCache = function(filename, key) { - var cache = null; - if (filename) { - try { - var cache = fs.readFileSync(filename, "utf8"); - cache = JSON.parse(cache)[key]; - if (!cache) throw "init"; - cache.props = UglifyJS.Dictionary.fromObject(cache.props); - } catch(ex) { - cache = { - cname: -1, - props: new UglifyJS.Dictionary() - }; - } - } - return cache; -}; - -UglifyJS.writeNameCache = function(filename, key, cache) { - if (filename) { - var data; - try { - data = fs.readFileSync(filename, "utf8"); - data = JSON.parse(data); - } catch(ex) { - data = {}; - } - data[key] = { - cname: cache.cname, - props: cache.props.toObject() - }; - fs.writeFileSync(filename, JSON.stringify(data, null, 2), "utf8"); - } -}; - -// A file glob function that only supports "*" and "?" wildcards in the basename. -// Example: "foo/bar/*baz??.*.js" -// Argument `glob` may be a string or an array of strings. -// Returns an array of strings. Garbage in, garbage out. -UglifyJS.simple_glob = function simple_glob(glob) { - if (Array.isArray(glob)) { - return [].concat.apply([], glob.map(simple_glob)); - } - if (glob.match(/\*|\?/)) { - var dir = path.dirname(glob); - try { - var entries = fs.readdirSync(dir); - } catch (ex) {} - if (entries) { - var pattern = "^" + path.basename(glob) - .replace(/[.+^$[\]\\(){}]/g, "\\$&") - .replace(/\*/g, "[^/\\\\]*") - .replace(/\?/g, "[^/\\\\]") + "$"; - var mod = process.platform === "win32" ? "i" : ""; - var rx = new RegExp(pattern, mod); - var results = entries.filter(function(name) { - return rx.test(name); - }).map(function(name) { - return path.join(dir, name); - }); - if (results.length) return results; - } - } - return [ glob ]; -}; From 251ff1d1af3209af99d37007691acd5a3b771cfb Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 16 Apr 2017 04:04:28 +0800 Subject: [PATCH 04/36] update README (#1813) - mention major version bump - remove reference to internal API --- README.md | 45 +++++++-------------------------------------- bin/uglifyjs | 2 +- 2 files changed, 8 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 42d2e87d..672c2f3b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -UglifyJS 2 -========== +UglifyJS +======== [![Build Status](https://travis-ci.org/mishoo/UglifyJS2.svg)](https://travis-ci.org/mishoo/UglifyJS2) UglifyJS is a JavaScript parser, minifier, compressor or beautifier toolkit. #### Note: +- `uglify-js 3.x` is incompatible with the [`2.x` branch](https://github.com/mishoo/UglifyJS2/tree/v2.x). - release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify ES2015+ (ES6+) code then please use the [harmony](#harmony) development branch. - Node 7 has a known performance regression and runs `uglify-js` twice as slow. @@ -34,7 +35,7 @@ Usage uglifyjs [input files] [options] -UglifyJS2 can take multiple input files. It's recommended that you pass the +UglifyJS can take multiple input files. It's recommended that you pass the input files first, then pass the options. UglifyJS will parse input files in sequence and apply any compression options. The files are parsed in the same global scope, that is, a reference from a file to some @@ -117,7 +118,7 @@ The available options are: --keep-fnames Do not mangle/drop function names. Useful for code relying on Function.prototype.name. --name-cache File to hold mangled name mappings. - --self Build UglifyJS2 as a library (implies --wrap UglifyJS) + --self Build UglifyJS as a library (implies --wrap UglifyJS) --source-map [options] Enable source map/specify source map options: `base` Path to compute relative paths from input files. `content` Input source map, useful if you're compressing @@ -148,7 +149,7 @@ goes to STDOUT. ## Source map options -UglifyJS2 can generate a source map file, which is highly useful for +UglifyJS can generate a source map file, which is highly useful for debugging your compressed JavaScript. To get a source map, pass `--source-map --output output.js` (source map will be written out to `output.js.map`). @@ -554,7 +555,7 @@ needs to be kept in the output) are comments attached to toplevel nodes. ## Support for the SpiderMonkey AST -UglifyJS2 has its own abstract syntax tree format; for +UglifyJS has its own abstract syntax tree format; for [practical reasons](http://lisperator.net/blog/uglifyjs-why-not-switching-to-spidermonkey-ast/) we can't easily change to using the SpiderMonkey AST internally. However, UglifyJS now has a converter which can import a SpiderMonkey AST. @@ -580,38 +581,6 @@ Acorn is really fast (e.g. 250ms instead of 380ms on some 650K code), but converting the SpiderMonkey tree that Acorn produces takes another 150ms so in total it's a bit more than just using UglifyJS's own parser. -### Using UglifyJS to transform SpiderMonkey AST - -Now you can use UglifyJS as any other intermediate tool for transforming -JavaScript ASTs in SpiderMonkey format. - -Example: - -```javascript -function uglify(ast, options, mangle) { - // Conversion from SpiderMonkey AST to internal format - var uAST = UglifyJS.AST_Node.from_mozilla_ast(ast); - - // Compression - uAST.figure_out_scope(); - uAST = UglifyJS.Compressor(options).compress(uAST); - - // Mangling (optional) - if (mangle) { - uAST.figure_out_scope(); - uAST.compute_char_frequency(); - uAST.mangle_names(); - } - - // Back-conversion to SpiderMonkey AST - return uAST.to_mozilla_ast(); -} -``` - -Check out -[original blog post](http://rreverser.com/using-mozilla-ast-with-uglifyjs/) -for details. - API Reference ------------- diff --git a/bin/uglifyjs b/bin/uglifyjs index 96eef27e..f31529b0 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -36,7 +36,7 @@ program.option("-d, --define [=value]", "Global definitions.", parse_js("d program.option("--ie8", "Support non-standard Internet Explorer 8."); program.option("--keep-fnames", "Do not mangle/drop function names. Useful for code relying on Function.prototype.name."); program.option("--name-cache ", "File to hold mangled name mappings."); -program.option("--self", "Build UglifyJS2 as a library (implies --wrap UglifyJS)"); +program.option("--self", "Build UglifyJS as a library (implies --wrap UglifyJS)"); program.option("--source-map [options]", "Enable source map/specify source map options.", parse_source_map()); program.option("--stats", "Display operations run time on STDERR.") program.option("--toplevel", "Compress and/or mangle variables in toplevel scope."); From 44dfa5a318d6df8eff4741a351f24667708a9cab Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 16 Apr 2017 17:25:39 +0800 Subject: [PATCH 05/36] fix variable substitution (#1816) - let `collapse_vars` take care of value containing any symbols - improve overhead accounting --- lib/compress.js | 38 +++++++++++++++++++++----- test/compress/issue-1656.js | 6 ++--- test/compress/reduce_vars.js | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index c8b15ffc..6062be54 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3553,7 +3553,9 @@ merge(Compressor.prototype, { return make_node(AST_Infinity, self).optimize(compressor); } } - if (compressor.option("evaluate") && compressor.option("reduce_vars")) { + if (compressor.option("evaluate") + && compressor.option("reduce_vars") + && is_lhs(self, compressor.parent()) !== self) { var d = self.definition(); var fixed = self.fixed_value(); if (fixed) { @@ -3561,21 +3563,45 @@ merge(Compressor.prototype, { var init = fixed.evaluate(compressor); if (init !== fixed) { init = make_node_from_constant(init, fixed); - var value = best_of_expression(init.optimize(compressor), fixed).print_to_string().length; + var value = init.optimize(compressor).print_to_string().length; + var fn; + if (has_symbol_ref(fixed)) { + fn = function() { + var result = init.optimize(compressor); + return result === init ? result.clone(true) : result; + }; + } else { + value = Math.min(value, fixed.print_to_string().length); + fn = function() { + var result = best_of_expression(init.optimize(compressor), fixed); + return result === init || result === fixed ? result.clone(true) : result; + }; + } 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 : false; + var overhead = 0; + if (compressor.option("unused") && (!d.global || compressor.option("toplevel"))) { + overhead = (name + 2 + value) / d.references.length; + } + d.should_replace = value <= name + overhead ? fn : false; } else { d.should_replace = false; } } if (d.should_replace) { - return best_of_expression(d.should_replace.optimize(compressor), fixed).clone(true); + return d.should_replace(); } } } return self; + + function has_symbol_ref(value) { + var found; + value.walk(new TreeWalker(function(node) { + if (node instanceof AST_SymbolRef) found = true; + if (found) return true; + })); + return found; + } }); function is_atomic(lhs, self) { diff --git a/test/compress/issue-1656.js b/test/compress/issue-1656.js index 8b683a28..c4c8f863 100644 --- a/test/compress/issue-1656.js +++ b/test/compress/issue-1656.js @@ -35,11 +35,11 @@ f7: { console.log(a, b); } expect_exact: [ - "var a = 100, b = 10;", + "var b = 10;", "", "!function() {", - " for (;b = a, !1; ) ;", - "}(), console.log(a, b);", + " for (;b = 100, !1; ) ;", + "}(), console.log(100, b);", ] expect_stdout: true } diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index b6f711ad..7621dd4a 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1995,3 +1995,55 @@ catch_var: { } expect_stdout: "true" } + +issue_1814_1: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + const a = 42; + !function() { + var b = a; + !function(a) { + console.log(a++, b); + }(0); + }(); + } + expect: { + const a = 42; + !function() { + !function(a) { + console.log(a++, 42); + }(0); + }(); + } + expect_stdout: "0 42" +} + +issue_1814_2: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + const a = "32"; + !function() { + var b = a + 1; + !function(a) { + console.log(a++, b); + }(0); + }(); + } + expect: { + const a = "32"; + !function() { + !function(a) { + console.log(a++, "321"); + }(0); + }(); + } + expect_stdout: "0 '321'" +} From 1a498db2d3e520d1711144c423312c62a2673115 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 17 Apr 2017 01:36:50 +0800 Subject: [PATCH 06/36] enhance `reduce_vars` (#1814) - allow immediate assignment after declaration of variable - relax modification rule for immutable value - fix order of visit for TreeWalker - remove extraneous code --- lib/ast.js | 12 +-- lib/compress.js | 74 ++++++++++++------ test/compress/reduce_vars.js | 140 +++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 33 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0fa051b8..739c21c2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -182,21 +182,13 @@ var AST_BlockStatement = DEFNODE("BlockStatement", null, { }, AST_Block); var AST_EmptyStatement = DEFNODE("EmptyStatement", null, { - $documentation: "The empty statement (empty block or simply a semicolon)", - _walk: function(visitor) { - return visitor._visit(this); - } + $documentation: "The empty statement (empty block or simply a semicolon)" }, AST_Statement); var AST_StatementWithBody = DEFNODE("StatementWithBody", "body", { $documentation: "Base class for all statements that contain one nested body: `For`, `ForIn`, `Do`, `While`, `With`", $propdoc: { body: "[AST_Statement] the body; this should always be present, even if it's an AST_EmptyStatement" - }, - _walk: function(visitor) { - return visitor._visit(this, function(){ - this.body._walk(visitor); - }); } }, AST_Statement); @@ -551,11 +543,11 @@ var AST_Call = DEFNODE("Call", "expression args", { }, _walk: function(visitor) { return visitor._visit(this, function(){ - this.expression._walk(visitor); var args = this.args; for (var i = 0, len = args.length; i < len; i++) { args[i]._walk(visitor); } + this.expression._walk(visitor); }); } }); diff --git a/lib/compress.js b/lib/compress.js index 6062be54..640fcecc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -119,7 +119,7 @@ merge(Compressor.prototype, { option: function(key) { return this.options[key] }, compress: function(node) { if (this.option("expression")) { - node = node.process_expression(true); + node.process_expression(true); } var passes = +this.options.passes || 1; for (var pass = 0; pass < passes && pass < 3; ++pass) { @@ -128,7 +128,7 @@ merge(Compressor.prototype, { node = node.transform(this); } if (this.option("expression")) { - node = node.process_expression(false); + node.process_expression(false); } return node; }, @@ -200,7 +200,7 @@ merge(Compressor.prototype, { return this.TYPE == node.TYPE && this.print_to_string() == node.print_to_string(); }); - AST_Node.DEFMETHOD("process_expression", function(insert, compressor) { + AST_Scope.DEFMETHOD("process_expression", function(insert, compressor) { var self = this; var tt = new TreeTransformer(function(node) { if (insert && node instanceof AST_SimpleStatement) { @@ -244,10 +244,10 @@ merge(Compressor.prototype, { } return node; }); - return self.transform(tt); + self.transform(tt); }); - AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan){ + AST_Node.DEFMETHOD("reset_opt_flags", function(compressor, rescan) { var reduce_vars = rescan && compressor.option("reduce_vars"); var toplevel = compressor.option("toplevel"); var safe_ids = Object.create(null); @@ -258,7 +258,7 @@ merge(Compressor.prototype, { d.fixed = false; } }); - var tw = new TreeWalker(function(node, descend){ + var tw = new TreeWalker(function(node, descend) { node._squeezed = false; node._optimized = false; if (reduce_vars) { @@ -268,7 +268,7 @@ merge(Compressor.prototype, { var d = node.definition(); d.references.push(node); if (d.fixed === undefined || !is_safe(d) - || is_modified(node, 0, node.fixed_value() instanceof AST_Lambda)) { + || is_modified(node, 0, is_immutable(node.fixed_value()))) { d.fixed = false; } } @@ -293,6 +293,20 @@ merge(Compressor.prototype, { d.fixed = false; } } + if (node instanceof AST_Assign + && node.operator == "=" + && node.left instanceof AST_SymbolRef) { + var d = node.left.definition(); + if (HOP(safe_ids, d.id) && d.fixed == null) { + d.fixed = function() { + return node.right; + }; + mark(d, false); + node.right.walk(tw); + mark(d, true); + return true; + } + } if (node instanceof AST_Defun) { var d = node.name.definition(); if (!toplevel && d.global || is_safe(d)) { @@ -309,21 +323,24 @@ merge(Compressor.prototype, { } var iife; if (node instanceof AST_Function - && !node.name && (iife = tw.parent()) instanceof AST_Call && iife.expression === node) { - // Virtually turn IIFE parameters into variable definitions: - // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() - // So existing transformation rules can work on them. - node.argnames.forEach(function(arg, i) { - var d = arg.definition(); - d.fixed = function() { - return iife.args[i] || make_node(AST_Undefined, iife); - }; - mark(d, true); - }); + if (node.name) { + node.name.definition().fixed = node; + } else { + // Virtually turn IIFE parameters into variable definitions: + // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() + // So existing transformation rules can work on them. + node.argnames.forEach(function(arg, i) { + var d = arg.definition(); + d.fixed = function() { + return iife.args[i] || make_node(AST_Undefined, iife); + }; + mark(d, true); + }); + } } - if (node instanceof AST_If || node instanceof AST_DWLoop) { + if (node instanceof AST_If) { node.condition.walk(tw); push(); node.body.walk(tw); @@ -335,6 +352,13 @@ merge(Compressor.prototype, { } return true; } + if (node instanceof AST_DWLoop) { + push(); + node.condition.walk(tw); + node.body.walk(tw); + pop(); + return true; + } if (node instanceof AST_LabeledStatement) { push(); node.body.walk(tw); @@ -401,13 +425,17 @@ merge(Compressor.prototype, { def.should_replace = undefined; } - function is_modified(node, level, func) { + function is_immutable(value) { + return value && value.is_constant() || value instanceof AST_Lambda; + } + + function is_modified(node, level, immutable) { var parent = tw.parent(level); if (is_lhs(node, parent) - || !func && parent instanceof AST_Call && parent.expression === node) { + || !immutable && parent instanceof AST_Call && parent.expression === node) { return true; } else if (parent instanceof AST_PropAccess && parent.expression === node) { - return !func && is_modified(parent, level + 1); + return !immutable && is_modified(parent, level + 1); } } }); @@ -2167,7 +2195,7 @@ merge(Compressor.prototype, { if (this.expression instanceof AST_Function && (!this.expression.name || !this.expression.name.definition().references.length)) { var node = this.clone(); - node.expression = node.expression.process_expression(false, compressor); + node.expression.process_expression(false, compressor); return node; } return this; diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 7621dd4a..6e079c1a 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1996,6 +1996,146 @@ catch_var: { expect_stdout: "true" } +var_assign_1: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + !function() { + var a; + a = 2; + console.log(a); + }(); + } + expect: { + !function() { + console.log(2); + }(); + } + expect_stdout: "2" +} + +var_assign_2: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + !function() { + var a; + if (a = 2) console.log(a); + }(); + } + expect: { + !function() { + if (2) console.log(2); + }(); + } + expect_stdout: "2" +} + +var_assign_3: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + !function() { + var a; + while (a = 2); + console.log(a); + }(); + } + expect: { + !function() { + var a; + while (a = 2); + console.log(a); + }(); + } +} + +var_assign_4: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + !function a() { + a = 2; + console.log(a); + }(); + } + expect: { + !function a() { + a = 2, + console.log(a); + }(); + } +} + +var_assign_5: { + options = { + evaluate: true, + reduce_vars: true, + sequences: true, + side_effects: true, + unused: true, + } + input: { + !function() { + var a; + !function(b) { + a = 2; + console.log(a, b); + }(a); + }(); + } + expect: { + !function() { + var a; + !function(b) { + a = 2, + console.log(a, b); + }(a); + }(); + } + expect_stdout: "2 undefined" +} + +immutable: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a = "test"; + console.log(a.indexOf("e")); + }(); + } + expect: { + !function() { + console.log("test".indexOf("e")); + }(); + } + expect_stdout: "1" +} + issue_1814_1: { options = { evaluate: true, From 71a8d0d236d29015df2d1df18de11d661d17af2e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 17 Apr 2017 14:03:29 +0800 Subject: [PATCH 07/36] fix `reduce_vars` within try-block (#1818) Possible partial execution due to exceptions. --- lib/compress.js | 14 +++++++++++++- test/compress/reduce_vars.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 640fcecc..cc42c46e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -382,7 +382,19 @@ merge(Compressor.prototype, { pop(); return true; } - if (node instanceof AST_Catch || node instanceof AST_SwitchBranch) { + if (node instanceof AST_Try) { + push(); + walk_body(node, tw); + pop(); + if (node.bcatch) { + push(); + node.bcatch.walk(tw); + pop(); + } + if (node.bfinally) node.bfinally.walk(tw); + return true; + } + if (node instanceof AST_SwitchBranch) { push(); descend(); pop(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 6e079c1a..405dbc23 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2187,3 +2187,34 @@ issue_1814_2: { } expect_stdout: "0 '321'" } + +try_abort: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + try { + var a = 1; + throw ""; + var b = 2; + } catch (e) { + } + console.log(a, b); + }(); + } + expect: { + !function() { + try { + var a = 1; + throw ""; + var b = 2; + } catch (e) { + } + console.log(a, b); + }(); + } + expect_stdout: "1 undefined" +} From 4ffb6fce7668a1199284e4ce8be91fdaeaf2df0e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 17 Apr 2017 17:11:29 +0800 Subject: [PATCH 08/36] compress duplicated variable definitions (#1817) These are surprisingly common, as people reuse the same variable name within loops or switch branches. --- lib/compress.js | 52 ++++++++++++++++++++++++++++++------ lib/scope.js | 2 +- test/compress/drop-unused.js | 27 +++++++++++++++++++ test/compress/reduce_vars.js | 3 +-- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index cc42c46e..7324fe0e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1813,6 +1813,7 @@ merge(Compressor.prototype, { } }); } + var var_defs_by_id = new Dictionary(); var initializations = new Dictionary(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). @@ -1832,8 +1833,11 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && scope === self) { node.definitions.forEach(function(def){ + var node_def = def.name.definition(); + if (def.name instanceof AST_SymbolVar) { + var_defs_by_id.add(node_def.id, def); + } if (!drop_vars) { - var node_def = def.name.definition(); if (!(node_def.id in in_use_ids)) { in_use_ids[node_def.id] = true; in_use.push(node_def); @@ -1943,19 +1947,29 @@ merge(Compressor.prototype, { } if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ - if (def.value) def.value = def.value.transform(tt); - var sym = def.name.definition(); - if (sym.id in in_use_ids) return true; - if (sym.orig[0] instanceof AST_SymbolCatch) { - def.value = def.value && def.value.drop_side_effect_free(compressor); - return true; - } var w = { name : def.name.name, file : def.name.start.file, line : def.name.start.line, col : def.name.start.col }; + if (def.value) def.value = def.value.transform(tt); + var sym = def.name.definition(); + if (sym.id in in_use_ids) { + if (def.name instanceof AST_SymbolVar) { + var var_defs = var_defs_by_id.get(sym.id); + if (var_defs.length > 1 && !def.value) { + compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", w); + var_defs.splice(var_defs.indexOf(def), 1); + return false; + } + } + return true; + } + if (sym.orig[0] instanceof AST_SymbolCatch) { + def.value = def.value && def.value.drop_side_effect_free(compressor); + return true; + } if (def.value && (def._unused_side_effects = def.value.drop_side_effect_free(compressor))) { compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", w); return true; @@ -1963,6 +1977,28 @@ merge(Compressor.prototype, { compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", w); return false; }); + if (def.length == 1 + && def[0].value + && !def[0]._unused_side_effects + && def[0].name instanceof AST_SymbolVar) { + var var_defs = var_defs_by_id.get(def[0].name.definition().id); + if (var_defs.length > 1) { + compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", { + name : def[0].name.name, + file : def[0].name.start.file, + line : def[0].name.start.line, + col : def[0].name.start.col + }); + var_defs.splice(var_defs.indexOf(def[0]), 1); + return make_node(AST_SimpleStatement, node, { + body: make_node(AST_Assign, def[0], { + operator: "=", + left: make_node(AST_SymbolRef, def[0].name, def[0].name), + right: def[0].value + }) + }); + } + } // place uninitialized names at the start def = mergeSort(def, function(a, b){ if (!a.value && b.value) return -1; diff --git a/lib/scope.js b/lib/scope.js index 2ffca25a..c8d7c6bb 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -268,7 +268,7 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(parent_scope){ AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; - this.def_variable(new AST_SymbolVar({ + this.def_variable(new AST_SymbolConst({ name: "arguments", start: this.start, end: this.end diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 99d9cace..2eefbe8d 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1029,3 +1029,30 @@ delete_assign_2: { } expect_stdout: true } + +drop_var: { + options = { + toplevel: true, + unused: true, + } + input: { + var a; + console.log(a, b); + var a = 1, b = 2; + console.log(a, b); + var a = 3; + console.log(a, b); + } + expect: { + console.log(a, b); + var a = 1, b = 2; + console.log(a, b); + a = 3; + console.log(a, b); + } + expect_stdout: [ + "undefined undefined", + "1 2", + "3 2", + ] +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 405dbc23..82b00211 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -1639,7 +1639,7 @@ redefine_arguments_1: { return typeof arguments; } function g() { - return"number"; + return "number"; } function h(x) { var arguments = x; @@ -1951,7 +1951,6 @@ pure_getters_2: { var a = a && a.b; } expect: { - var a; var a = a && a.b; } } From 6d5f341999da7dfad708979151932fd9d8242ebd Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 17 Apr 2017 17:24:29 +0800 Subject: [PATCH 09/36] fix `reduce_vars` on boolean binary expressions (#1819) Side effects of `&&` and `||` have not mattered until #1814, which takes assignment expressions into account. --- lib/compress.js | 8 ++++++++ test/compress/reduce_vars.js | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 7324fe0e..f49dd60e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -340,6 +340,14 @@ merge(Compressor.prototype, { }); } } + if (node instanceof AST_Binary + && (node.operator == "&&" || node.operator == "||")) { + node.left.walk(tw); + push(); + node.right.walk(tw); + pop(); + return true; + } if (node instanceof AST_If) { node.condition.walk(tw); push(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 82b00211..94d37cb7 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2217,3 +2217,26 @@ try_abort: { } expect_stdout: "1 undefined" } + +boolean_binary_assign: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a; + void 0 && (a = 1); + console.log(a); + }(); + } + expect: { + !function() { + var a; + void 0; + console.log(a); + }(); + } + expect_stdout: "undefined" +} From d1aa09c5c7af14bf5f17cc7ea2ab5d6be20e3220 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 18 Apr 2017 01:44:23 +0800 Subject: [PATCH 10/36] fix `reduce_vars` on conditionals (#1822) --- lib/compress.js | 10 ++++++++++ test/compress/reduce_vars.js | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index f49dd60e..0dfe2a3c 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -348,6 +348,16 @@ merge(Compressor.prototype, { pop(); return true; } + if (node instanceof AST_Conditional) { + node.condition.walk(tw); + push(); + node.consequent.walk(tw); + pop(); + push(); + node.alternative.walk(tw); + pop(); + return true; + } if (node instanceof AST_If) { node.condition.walk(tw); push(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index 94d37cb7..ad2c90bc 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -2240,3 +2240,26 @@ boolean_binary_assign: { } expect_stdout: "undefined" } + +cond_assign: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a; + void 0 ? (a = 1) : 0; + console.log(a); + }(); + } + expect: { + !function() { + var a; + void 0 ? (a = 1) : 0; + console.log(a); + }(); + } + expect_stdout: "undefined" +} From 5d9f1da3abc58bce95dd240bd586bedb4eb04771 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 18 Apr 2017 13:38:42 +0800 Subject: [PATCH 11/36] support safe reassignments in `reduce_vars` (#1823) `var a=1;a=2;x(a)` => `x(2)` fix pre-existing issues - reference counting on assignment - walking of anonymous functions - chained assignment --- lib/compress.js | 51 ++++++++++++----- test/compress/collapse_vars.js | 22 +++++++ test/compress/reduce_vars.js | 102 +++++++++++++++++++++++++++------ 3 files changed, 145 insertions(+), 30 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 0dfe2a3c..596b03fa 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -267,7 +267,7 @@ merge(Compressor.prototype, { if (node instanceof AST_SymbolRef) { var d = node.definition(); d.references.push(node); - if (d.fixed === undefined || !is_safe(d) + if (d.fixed === undefined || !safe_to_read(d) || is_modified(node, 0, is_immutable(node.fixed_value()))) { d.fixed = false; } @@ -277,7 +277,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_VarDef) { var d = node.name.definition(); - if (d.fixed == null) { + if (d.fixed === undefined || safe_to_assign(d, node.value)) { if (node.value) { d.fixed = function() { return node.value; @@ -297,7 +297,8 @@ merge(Compressor.prototype, { && node.operator == "=" && node.left instanceof AST_SymbolRef) { var d = node.left.definition(); - if (HOP(safe_ids, d.id) && d.fixed == null) { + if (safe_to_assign(d, node.right)) { + d.references.push(node.left); d.fixed = function() { return node.right; }; @@ -309,7 +310,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Defun) { var d = node.name.definition(); - if (!toplevel && d.global || is_safe(d)) { + if (!toplevel && d.global || safe_to_read(d)) { d.fixed = false; } else { d.fixed = node; @@ -321,13 +322,12 @@ merge(Compressor.prototype, { safe_ids = save_ids; return true; } - var iife; - if (node instanceof AST_Function - && (iife = tw.parent()) instanceof AST_Call - && iife.expression === node) { - if (node.name) { - node.name.definition().fixed = node; - } else { + if (node instanceof AST_Function) { + push(); + var iife; + if (!node.name + && (iife = tw.parent()) instanceof AST_Call + && iife.expression === node) { // Virtually turn IIFE parameters into variable definitions: // (function(a,b) {...})(c,d) => (function() {var a=c,b=d; ...})() // So existing transformation rules can work on them. @@ -339,6 +339,9 @@ merge(Compressor.prototype, { mark(d, true); }); } + descend(); + pop(); + return true; } if (node instanceof AST_Binary && (node.operator == "&&" || node.operator == "||")) { @@ -385,11 +388,19 @@ merge(Compressor.prototype, { } if (node instanceof AST_For) { if (node.init) node.init.walk(tw); + if (node.condition) { + push(); + node.condition.walk(tw); + pop(); + } push(); - if (node.condition) node.condition.walk(tw); node.body.walk(tw); - if (node.step) node.step.walk(tw); pop(); + if (node.step) { + push(); + node.step.walk(tw); + pop(); + } return true; } if (node instanceof AST_ForIn) { @@ -426,7 +437,7 @@ merge(Compressor.prototype, { safe_ids[def.id] = safe; } - function is_safe(def) { + function safe_to_read(def) { if (safe_ids[def.id]) { if (def.fixed == null) { var orig = def.orig[0]; @@ -437,6 +448,18 @@ merge(Compressor.prototype, { } } + function safe_to_assign(def, value) { + if (!HOP(safe_ids, def.id)) return false; + if (!safe_to_read(def)) return false; + if (def.fixed === false) return false; + if (def.fixed != null && (!value || def.references.length > 0)) return false; + return !def.orig.some(function(sym) { + return sym instanceof AST_SymbolConst + || sym instanceof AST_SymbolDefun + || sym instanceof AST_SymbolLambda; + }); + } + function push() { safe_ids = Object.create(safe_ids); } diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 2264783d..c01572dd 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1592,3 +1592,25 @@ var_side_effects_3: { } expect_stdout: true } + +reduce_vars_assign: { + options = { + collapse_vars: true, + reduce_vars: true, + } + input: { + !function() { + var a = 1; + a = [].length, + console.log(a); + }(); + } + expect: { + !function() { + var a = 1; + a = [].length, + console.log(a); + }(); + } + expect_stdout: "0" +} diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index ad2c90bc..c5f26904 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -66,7 +66,7 @@ modified: { conditionals : true, evaluate : true, reduce_vars : true, - unused : true + unused : true, } input: { function f0() { @@ -136,12 +136,11 @@ modified: { } function f2() { - var b = 2; - b = 3; - console.log(1 + b); - console.log(b + 3); + 3; console.log(4); - console.log(1 + b + 3); + console.log(6); + console.log(4); + console.log(7); } function f3() { @@ -375,12 +374,11 @@ passes: { } expect: { function f() { - var b = 2; - b = 3; - console.log(1 + b); - console.log(b + 3); + 3; console.log(4); - console.log(1 + b + 3); + console.log(6); + console.log(4); + console.log(7); } } } @@ -573,7 +571,7 @@ inner_var_label: { } } -inner_var_for: { +inner_var_for_1: { options = { evaluate: true, reduce_vars: true, @@ -602,6 +600,29 @@ inner_var_for: { } } +inner_var_for_2: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a = 1; + for (var b = 1; --b;) var a = 2; + console.log(a); + }(); + } + expect: { + !function() { + a = 1; + for (var b = 1; --b;) var a = 2; + console.log(a); + }(); + } + expect_stdout: "1" +} + inner_var_for_in_1: { options = { evaluate: true, @@ -1828,10 +1849,7 @@ redefine_farg_3: { console.log(function(a) { var a; return typeof a; - }([]), "number", function(a) { - var a = void 0; - return typeof a; - }([])); + }([]), "number", "undefined"); } expect_stdout: "object number undefined" } @@ -2115,6 +2133,27 @@ var_assign_5: { expect_stdout: "2 undefined" } +var_assign_6: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a = function(){}(a = 1); + console.log(a); + }(); + } + expect: { + !function() { + var a = function(){}(a = 1); + console.log(a); + }(); + } + expect_stdout: "undefined" +} + immutable: { options = { evaluate: true, @@ -2263,3 +2302,34 @@ cond_assign: { } expect_stdout: "undefined" } + +iife_assign: { + options = { + evaluate: true, + reduce_vars: true, + unused: true, + } + input: { + !function() { + var a = 1, b = 0; + !function() { + b++; + return; + a = 2; + }(); + console.log(a); + }(); + } + expect: { + !function() { + var a = 1, b = 0; + !function() { + b++; + return; + a = 2; + }(); + console.log(a); + }(); + } + expect_stdout: "1" +} From 0f4f01b66cb2aa4356309c70a7d6a95618603630 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Tue, 18 Apr 2017 21:45:34 +0800 Subject: [PATCH 12/36] clean up `collapse_vars` (#1826) - remove overlap in functionality of singular, consecutive reference of constant value - remove workarounds for previous bugs in `lib/scope.js` - distribute recursive `collapse_single_use_vars()` calls to their respective `OPT(AST_Node)` - enable collapsing of variables within a single `AST_Definitions` --- lib/compress.js | 251 ++++++++++++++++----------------- test/compress/collapse_vars.js | 104 +++++++++----- 2 files changed, 192 insertions(+), 163 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 596b03fa..a3641573 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -642,151 +642,134 @@ merge(Compressor.prototype, { // and if it has exactly one reference then attempt to replace its reference // in the statement with the var value and then erase the var definition. - var self = compressor.self(); - var var_defs_removed = false; + var scope = compressor.find_parent(AST_Scope); 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; - // Process child blocks of statement if present. - [stat, stat.body, stat.alternative, stat.bcatch, stat.bfinally].forEach(function(node) { - node && node.body && collapse_single_use_vars(node.body, compressor); - }); - - // The variable definition must precede a statement. - if (stat_index <= 0) break; - var prev_stat_index = stat_index - 1; - var prev_stat = statements[prev_stat_index]; - if (!(prev_stat instanceof AST_Definitions)) continue; - var var_defs = prev_stat.definitions; - if (var_defs == null) continue; - - var var_names_seen = {}; + var var_names_seen = Object.create(null); var side_effects_encountered = false; var lvalues_encountered = false; - var lvalues = {}; + var lvalues = Object.create(null); + var prev_stat_index, var_defs, var_defs_index; // Scan variable definitions from right to left. - for (var var_defs_index = var_defs.length; --var_defs_index >= 0;) { - - // Obtain var declaration and var name with basic sanity check. - var var_decl = var_defs[var_defs_index]; - if (var_decl.value == null) break; - var var_name = var_decl.name.name; - if (!var_name || !var_name.length) break; - - // Bail if we've seen a var definition of same name before. - if (var_name in var_names_seen) break; - var_names_seen[var_name] = true; - - // 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" || (!toplevel && def.global)) { - side_effects_encountered = true; - continue; + if (stat instanceof AST_Definitions) { + prev_stat_index = stat_index; + var_defs = stat.definitions; + for (var_defs_index = var_defs.length - 1; --var_defs_index >= 0;) { + if (collapse(var_defs[var_defs_index + 1])) break; } - var ref = def.references[0]; - - // Don't replace ref if eval() or with statement in scope. - if (ref.scope.uses_eval || ref.scope.uses_with) break; - - // Constant single use vars can be replaced in any scope. - if (var_decl.value.is_constant()) { - var ctt = new TreeTransformer(function(node) { - var parent = ctt.parent(); - if (parent instanceof AST_IterationStatement - && (parent.condition === node || parent.init === node)) { - return node; - } - if (node === ref) - return replace_var(node, parent, true); - }); - stat.transform(ctt); - continue; + } else if (stat_index > 0) { + // The variable definition must precede a statement. + prev_stat_index = stat_index - 1; + var prev_stat = statements[prev_stat_index]; + if (!(prev_stat instanceof AST_Definitions)) continue; + var_defs = prev_stat.definitions; + for (var_defs_index = var_defs.length; --var_defs_index >= 0;) { + if (collapse(stat)) break; } - - // Restrict var replacement to constants if side effects encountered. - if (side_effects_encountered |= lvalues_encountered) continue; - - var value_has_side_effects = var_decl.value.has_side_effects(compressor); - // Non-constant single use vars can only be replaced in same scope. - if (ref.scope !== self) { - side_effects_encountered |= value_has_side_effects; - continue; - } - - // Detect lvalues in var value. - var tw = new TreeWalker(function(node){ - if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) { - lvalues[node.name] = lvalues_encountered = true; - } - }); - var_decl.value.walk(tw); - - // Replace the non-constant single use var in statement if side effect free. - var unwind = false; - var tt = new TreeTransformer( - function preorder(node) { - if (unwind) return node; - var parent = tt.parent(); - if (node instanceof AST_Lambda - || node instanceof AST_Try - || node instanceof AST_With - || node instanceof AST_Case - || node instanceof AST_IterationStatement - || (parent instanceof AST_If && node !== parent.condition) - || (parent instanceof AST_Conditional && node !== parent.condition) - || (node instanceof AST_SymbolRef - && value_has_side_effects - && !are_references_in_scope(node.definition(), self)) - || (parent instanceof AST_Binary - && (parent.operator == "&&" || parent.operator == "||") - && node === parent.right) - || (parent instanceof AST_Switch && node !== parent.expression)) { - return side_effects_encountered = unwind = true, node; - } - function are_references_in_scope(def, scope) { - if (def.orig.length === 1 - && def.orig[0] instanceof AST_SymbolDefun) return true; - if (def.scope !== scope) return false; - var refs = def.references; - for (var i = 0, len = refs.length; i < len; i++) { - if (refs[i].scope !== scope) return false; - } - return true; - } - }, - function postorder(node) { - if (unwind) return node; - if (node === ref) - return unwind = true, replace_var(node, tt.parent(), false); - if (side_effects_encountered |= node.has_side_effects(compressor)) - return unwind = true, node; - if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) { - side_effects_encountered = true; - return unwind = true, node; - } - } - ); - stat.transform(tt); } } - // Remove extraneous empty statments in block after removing var definitions. - // Leave at least one statement in `statements`. - if (var_defs_removed) for (var i = statements.length; --i >= 0;) { - if (statements.length > 1 && statements[i] instanceof AST_EmptyStatement) - statements.splice(i, 1); - } - return statements; + function collapse(stat) { + var var_decl = var_defs[var_defs_index]; + // `drop_unused()` shuffles variables without values to the top, + // so we can terminate upon first sighting as an optimization. + if (var_decl.value == null) return true; + var var_name = var_decl.name.name; + + // Bail if we've seen a var definition of same name before. + if (var_name in var_names_seen) return true; + var_names_seen[var_name] = true; + + // Only interested in non-constant values. + if (var_decl.value.is_constant()) return; + + // Only interested in cases with just one reference to the variable. + var def = var_decl.name.definition(); + if (def.references.length !== 1 + || var_name == "arguments" || (!toplevel && def.global)) { + side_effects_encountered = true; + return; + } + var ref = def.references[0]; + + // Don't replace ref if eval() or with statement in scope. + if (ref.scope.uses_eval || ref.scope.uses_with) return true; + + // Restrict var replacement to constants if side effects encountered. + if (side_effects_encountered |= lvalues_encountered) return; + + var value_has_side_effects = var_decl.value.has_side_effects(compressor); + // Non-constant single use vars can only be replaced in same scope. + if (ref.scope !== scope) { + side_effects_encountered |= value_has_side_effects; + return; + } + + // Detect lvalues in var value. + var tw = new TreeWalker(function(node){ + if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) { + lvalues[node.name] = lvalues_encountered = true; + } + }); + var_decl.value.walk(tw); + + // Replace the non-constant single use var in statement if side effect free. + var unwind = false; + var tt = new TreeTransformer( + function preorder(node) { + if (unwind || node instanceof AST_Scope && node !== scope) return node; + var parent = tt.parent(); + if (node instanceof AST_Try + || node instanceof AST_With + || node instanceof AST_Case + || node instanceof AST_IterationStatement + || (parent instanceof AST_If && node !== parent.condition) + || (parent instanceof AST_Conditional && node !== parent.condition) + || (node instanceof AST_SymbolRef + && value_has_side_effects + && !are_references_in_scope(node.definition(), scope)) + || (parent instanceof AST_Binary + && (parent.operator == "&&" || parent.operator == "||") + && node === parent.right) + || (parent instanceof AST_Switch && node !== parent.expression)) { + return side_effects_encountered = unwind = true, node; + } + function are_references_in_scope(def, scope) { + if (def.orig.length === 1 + && def.orig[0] instanceof AST_SymbolDefun) return true; + if (def.scope !== scope) return false; + var refs = def.references; + for (var i = 0, len = refs.length; i < len; i++) { + if (refs[i].scope !== scope) return false; + } + return true; + } + }, + function postorder(node) { + if (unwind) return node; + if (node === ref) + return unwind = true, replace_var(var_decl, node, tt.parent(), false); + if (side_effects_encountered |= node.has_side_effects(compressor)) + return unwind = true, node; + if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) { + side_effects_encountered = true; + return unwind = true, node; + } + } + ); + stat.transform(tt); + } + function is_lvalue(node, parent) { return node instanceof AST_SymbolRef && is_lhs(node, parent); } - function replace_var(node, parent, is_constant) { + + function replace_var(var_decl, node, parent, is_constant) { if (is_lvalue(node, parent)) return node; // Remove var definition and return its value to the TreeTransformer to replace. @@ -795,14 +778,19 @@ merge(Compressor.prototype, { var_defs.splice(var_defs_index, 1); if (var_defs.length === 0) { - statements[prev_stat_index] = make_node(AST_EmptyStatement, self); - var_defs_removed = true; + statements.splice(prev_stat_index, 1); + stat_index--; } // Further optimize statement after substitution. stat.reset_opt_flags(compressor); - compressor.info("Collapsing " + (is_constant ? "constant" : "variable") + - " " + var_name + " [{file}:{line},{col}]", node.start); + compressor.info("Collapsing {type} {name} [{file}:{line},{col}]", { + type: is_constant ? "constant" : "variable", + name: var_decl.name.name, + file: node.start.file, + line: node.start.line, + col: node.start.col + }); CHANGED = true; return value; } @@ -1746,6 +1734,7 @@ merge(Compressor.prototype, { def(AST_SymbolRef, function(compressor){ return this.undeclared(); }); + def(AST_SymbolDeclaration, return_false); def(AST_Object, function(compressor){ return any(this.properties, compressor); }); diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index c01572dd..0f82b743 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -2,7 +2,7 @@ collapse_vars_side_effects_1: { 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, reduce_vars:true } input: { function f1() { @@ -151,7 +151,7 @@ collapse_vars_issue_721: { 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, reduce_vars:true } input: { define(["require", "exports", 'handlebars'], function (require, exports, hb) { @@ -217,7 +217,7 @@ collapse_vars_properties: { 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, reduce_vars:true } input: { function f1(obj) { @@ -244,7 +244,7 @@ collapse_vars_if: { 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, reduce_vars:true } input: { function f1() { @@ -294,7 +294,7 @@ collapse_vars_while: { options = { collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, comparisons:true, evaluate:true, booleans:true, loops:false, 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, reduce_vars:true } input: { function f1(y) { @@ -393,9 +393,9 @@ collapse_vars_do_while: { } function f3(y) { function fn(n) { console.log(n); } - var a = 2; + var a = 2, x = 7; do { - fn(a = 7); + fn(a = x); break; } while (y); } @@ -468,8 +468,9 @@ collapse_vars_do_while_drop_assign: { } function f3(y) { function fn(n) { console.log(n); } + var x = 7; do { - fn(7); + fn(x); break; } while (y); } @@ -670,7 +671,7 @@ collapse_vars_lvalues: { function f4(x) { var a = (x -= 3); return x + a; } function f5(x) { var w = e1(), v = e2(), c = v = --x; return (w = x) - c; } function f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); } - function f7(x) { var w = e1(), v = e2(), c = v - x; return (w = x) - c; } + function f7(x) { var w = e1(), c = e2() - x; return (w = x) - c; } function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } function f9(x) { var w = e1(); return e2() - x - (w = x); } } @@ -702,7 +703,7 @@ collapse_vars_lvalues_drop_assign: { function f4(x) { var a = (x -= 3); return x + a; } function f5(x) { var v = (e1(), e2()), c = v = --x; return x - c; } function f6(x) { e1(), e2(); return --x - x; } - function f7(x) { var v = (e1(), e2()), c = v - x; return x - c; } + function f7(x) { var c = (e1(), e2() - x); return x - c; } function f8(x) { var v = (e1(), e2()); return x - (v - x); } function f9(x) { e1(); return e2() - x - x; } } @@ -712,7 +713,7 @@ collapse_vars_misc1: { 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, reduce_vars:true } input: { function f0(o, a, h) { @@ -789,7 +790,7 @@ collapse_vars_repeated: { 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, reduce_vars:true } input: { function f1() { @@ -812,19 +813,17 @@ collapse_vars_repeated: { } expect: { function f1() { - return -3 + return -3; } function f2(x) { - return x + return x; } (function(x){ - var a = "GOOD" + x, e = "BAD", e = a; - console.log(e + "!"); - })("!"), + console.log("GOOD!!"); + })(), (function(x){ - var a = "GOOD" + x, e = "BAD" + x, e = a; - console.log(e + "!"); - })("!"); + console.log("GOOD!!"); + })(); } expect_stdout: true } @@ -833,7 +832,7 @@ collapse_vars_closures: { 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, reduce_vars:true } input: { function constant_vars_can_be_replaced_in_any_scope() { @@ -923,7 +922,7 @@ collapse_vars_try: { 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, reduce_vars:true } input: { function f1() { @@ -1121,7 +1120,7 @@ collapse_vars_constants: { 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, reduce_vars:true } input: { function f1(x) { @@ -1159,7 +1158,7 @@ collapse_vars_arguments: { 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, - toplevel:true + toplevel:true, reduce_vars:true } input: { var outer = function() { @@ -1286,6 +1285,7 @@ collapse_vars_regexp: { join_vars: true, cascade: true, side_effects: true, + reduce_vars: true, } input: { function f1() { @@ -1319,8 +1319,8 @@ collapse_vars_regexp: { }; } (function(){ - var result, s = "acdabcdeabbb", rx = /ab*/g; - while (result = rx.exec(s)) + var result, rx = /ab*/g; + while (result = rx.exec("acdabcdeabbb")) console.log(result[0]); })(); } @@ -1344,7 +1344,10 @@ issue_1537: { issue_1562: { options = { collapse_vars: true, + evaluate: true, + reduce_vars: true, toplevel: true, + unused: true, } input: { var v = 1, B = 2; @@ -1363,14 +1366,11 @@ issue_1562: { var v = 1; for (v in objs) f(2); - var x = 3; - while(x + 2) bar(10); + while(5) bar(10); - var y = 4; - do bar(20); while(y + 2); + do bar(20); while(6); - var z = 5; - for (; f(z + 2) ;) bar(30); + for (; f(7) ;) bar(30); } } @@ -1614,3 +1614,43 @@ reduce_vars_assign: { } expect_stdout: "0" } + +iife_1: { + options = { + collapse_vars: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + var log = function(x) { + console.log(x); + }, foo = bar(); + log(foo); + } + expect: { + (function(x) { + console.log(x); + })(bar()); + } +} + +iife_2: { + options = { + collapse_vars: true, + reduce_vars: false, + toplevel: true, + unused: false, + } + input: { + var foo = bar(); + !function(x) { + console.log(x); + }(foo); + } + expect: { + !function(x) { + console.log(x); + }(bar()); + } +} From 28cfb65c47e7a2adeec35d8a78dd8bb0cf06af12 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 19 Apr 2017 04:17:15 +0800 Subject: [PATCH 13/36] extend `cascade` into `a.b` (#1829) fixes #27 --- lib/compress.js | 1 + test/compress/sequences.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index a3641573..eb54f75d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -3132,6 +3132,7 @@ merge(Compressor.prototype, { field = "left"; } } else if (cdr instanceof AST_Call + || cdr instanceof AST_PropAccess || cdr instanceof AST_Unary && !unary_side_effects(cdr.operator)) { field = "expression"; } else { diff --git a/test/compress/sequences.js b/test/compress/sequences.js index 3fb26278..10492565 100644 --- a/test/compress/sequences.js +++ b/test/compress/sequences.js @@ -688,3 +688,25 @@ side_effects_cascade_3: { } } } + +issue_27: { + options = { + cascade: true, + passes: 2, + sequences: true, + side_effects: true, + unused: true, + } + input: { + (function(jQuery) { + var $; + $ = jQuery; + $("body").addClass("foo"); + })(jQuery); + } + expect: { + (function(jQuery) { + jQuery("body").addClass("foo"); + })(jQuery); + } +} From b4b9305db0d3c4682848ed0a4214f1fee332a078 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 19 Apr 2017 04:27:13 +0800 Subject: [PATCH 14/36] fix parser bugs & CLI reporting (#1827) fixes #1825 --- bin/uglifyjs | 16 +++---- lib/parse.js | 27 +++++------- test/input/invalid/assign_4.js | 1 + test/input/invalid/dot_1.js | 1 + test/input/invalid/dot_2.js | 1 + test/input/invalid/dot_3.js | 1 + test/input/invalid/object.js | 1 + test/mocha/cli.js | 79 +++++++++++++++++++++++++++++++++- 8 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 test/input/invalid/assign_4.js create mode 100644 test/input/invalid/dot_1.js create mode 100644 test/input/invalid/dot_2.js create mode 100644 test/input/invalid/dot_3.js create mode 100644 test/input/invalid/object.js diff --git a/bin/uglifyjs b/bin/uglifyjs index f31529b0..e89b68a7 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -190,19 +190,19 @@ function run() { if (ex instanceof UglifyJS.JS_Parse_Error) { console.error("Parse error at " + ex.filename + ":" + ex.line + "," + ex.col); var col = ex.col; - var line = files[ex.filename].split(/\r?\n/)[ex.line - (col ? 1 : 2)]; + var lines = files[ex.filename].split(/\r?\n/); + var line = lines[ex.line - 1]; + if (!line && !col) { + line = lines[ex.line - 2]; + col = line.length; + } if (line) { if (col > 40) { line = line.slice(col - 40); col = 40; } - if (col) { - console.error(line.slice(0, 80)); - console.error(line.slice(0, col).replace(/\S/g, " ") + "^"); - } else { - console.error(line.slice(-40)); - console.error(line.slice(-40).replace(/\S/g, " ") + "^"); - } + console.error(line.slice(0, 80)); + console.error(line.slice(0, col).replace(/\S/g, " ") + "^"); } } fatal("ERROR: " + ex.message); diff --git a/lib/parse.js b/lib/parse.js index 27351b53..40528df1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -111,7 +111,7 @@ var WHITESPACE_CHARS = makePredicate(characters(" \u00a0\n\r\t\f\u000b\u200b\u20 var NEWLINE_CHARS = makePredicate(characters("\n\r\u2028\u2029")); -var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); +var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,;:")); var PUNC_CHARS = makePredicate(characters("[]{}(),;:")); @@ -1353,14 +1353,15 @@ function parse($TEXT, options) { function as_property_name() { var tmp = S.token; - next(); switch (tmp.type) { + case "operator": + if (!KEYWORDS(tmp.value)) unexpected(); case "num": case "string": case "name": - case "operator": case "keyword": case "atom": + next(); return tmp.value; default: unexpected(); @@ -1369,16 +1370,9 @@ function parse($TEXT, options) { function as_name() { var tmp = S.token; + if (tmp.type != "name") unexpected(); next(); - switch (tmp.type) { - case "name": - case "operator": - case "keyword": - case "atom": - return tmp.value; - default: - unexpected(); - } + return tmp.value; }; function _make_symbol(type) { @@ -1439,14 +1433,14 @@ function parse($TEXT, options) { if (is("operator") && UNARY_PREFIX(start.value)) { next(); handle_regexp(); - var ex = make_unary(AST_UnaryPrefix, start.value, maybe_unary(allow_calls)); + var ex = make_unary(AST_UnaryPrefix, start, maybe_unary(allow_calls)); ex.start = start; ex.end = prev(); return ex; } var val = expr_atom(allow_calls); while (is("operator") && UNARY_POSTFIX(S.token.value) && !S.token.nlb) { - val = make_unary(AST_UnaryPostfix, S.token.value, val); + val = make_unary(AST_UnaryPostfix, S.token, val); val.start = start; val.end = S.token; next(); @@ -1454,9 +1448,10 @@ function parse($TEXT, options) { return val; }; - function make_unary(ctor, op, expr) { + function make_unary(ctor, token, expr) { + var op = token.value; if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("Invalid use of " + op + " operator", null, ctor === AST_UnaryPrefix ? expr.start.col - 1 : null); + croak("Invalid use of " + op + " operator", token.line, token.col, token.pos); return new ctor({ operator: op, expression: expr }); }; diff --git a/test/input/invalid/assign_4.js b/test/input/invalid/assign_4.js new file mode 100644 index 00000000..d4d6b113 --- /dev/null +++ b/test/input/invalid/assign_4.js @@ -0,0 +1 @@ +++null diff --git a/test/input/invalid/dot_1.js b/test/input/invalid/dot_1.js new file mode 100644 index 00000000..7c4f3a66 --- /dev/null +++ b/test/input/invalid/dot_1.js @@ -0,0 +1 @@ +a.= diff --git a/test/input/invalid/dot_2.js b/test/input/invalid/dot_2.js new file mode 100644 index 00000000..32c027f8 --- /dev/null +++ b/test/input/invalid/dot_2.js @@ -0,0 +1 @@ +%.a; diff --git a/test/input/invalid/dot_3.js b/test/input/invalid/dot_3.js new file mode 100644 index 00000000..65573828 --- /dev/null +++ b/test/input/invalid/dot_3.js @@ -0,0 +1 @@ +a./(); diff --git a/test/input/invalid/object.js b/test/input/invalid/object.js new file mode 100644 index 00000000..46216d81 --- /dev/null +++ b/test/input/invalid/object.js @@ -0,0 +1 @@ +console.log({%: 1}); diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 7162c816..697c09a3 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -296,14 +296,89 @@ describe("bin/uglifyjs", function () { assert.ok(err); assert.strictEqual(stdout, ""); assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ - "Parse error at test/input/invalid/assign_3.js:1,18", + "Parse error at test/input/invalid/assign_3.js:1,17", "console.log(3 || ++this);", - " ^", + " ^", "ERROR: Invalid use of ++ operator" ].join("\n")); done(); }); }); + it("Should throw syntax error (++null)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/assign_4.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/assign_4.js:1,0", + "++null", + "^", + "ERROR: Invalid use of ++ operator" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (a.=)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/dot_1.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/dot_1.js:1,2", + "a.=", + " ^", + "ERROR: Unexpected token: operator (=)" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (%.a)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/dot_2.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/dot_2.js:1,0", + "%.a;", + "^", + "ERROR: Unexpected token: operator (%)" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (a./();)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/dot_3.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/dot_3.js:1,2", + "a./();", + " ^", + "ERROR: Unexpected token: operator (/)" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error ({%: 1})", function(done) { + var command = uglifyjscmd + ' test/input/invalid/object.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/object.js:1,13", + "console.log({%: 1});", + " ^", + "ERROR: Unexpected token: operator (%)" + ].join("\n")); + done(); + }); + }); it("Should handle literal string as source map input", function(done) { var command = [ uglifyjscmd, From 4dcff038cb1a6951a0b20d1345bfdb27d756301c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Wed, 19 Apr 2017 04:49:09 +0800 Subject: [PATCH 15/36] improve `collapse_vars` on `AST_Var` (#1828) Perform the same cascaded scanning within `var` statement as we do on array of statements. --- lib/compress.js | 195 +++++++++++++++++---------------- test/compress/collapse_vars.js | 23 ++++ 2 files changed, 123 insertions(+), 95 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index eb54f75d..2612d9ab 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -644,21 +644,21 @@ merge(Compressor.prototype, { var scope = compressor.find_parent(AST_Scope); var toplevel = compressor.option("toplevel"); - for (var stat_index = statements.length; --stat_index >= 0;) { - var stat = statements[stat_index]; - - var var_names_seen = Object.create(null); - var side_effects_encountered = false; - var lvalues_encountered = false; - var lvalues = Object.create(null); - var prev_stat_index, var_defs, var_defs_index; - + var stat_index; + var prev_stat_index; + var def_stat_index; + var stat; + var var_defs; + var var_defs_index; + for (stat_index = statements.length; --stat_index >= 0;) { + stat = statements[stat_index]; // Scan variable definitions from right to left. if (stat instanceof AST_Definitions) { prev_stat_index = stat_index; var_defs = stat.definitions; - for (var_defs_index = var_defs.length - 1; --var_defs_index >= 0;) { - if (collapse(var_defs[var_defs_index + 1])) break; + for (def_stat_index = var_defs.length; --def_stat_index >= 1;) { + stat = var_defs[def_stat_index]; + scan_var_defs(def_stat_index); } } else if (stat_index > 0) { // The variable definition must precede a statement. @@ -666,103 +666,107 @@ merge(Compressor.prototype, { var prev_stat = statements[prev_stat_index]; if (!(prev_stat instanceof AST_Definitions)) continue; var_defs = prev_stat.definitions; - for (var_defs_index = var_defs.length; --var_defs_index >= 0;) { - if (collapse(stat)) break; - } + scan_var_defs(var_defs.length); } } return statements; - function collapse(stat) { - var var_decl = var_defs[var_defs_index]; - // `drop_unused()` shuffles variables without values to the top, - // so we can terminate upon first sighting as an optimization. - if (var_decl.value == null) return true; - var var_name = var_decl.name.name; + function scan_var_defs(end_pos) { + var var_names_seen = Object.create(null); + var side_effects_encountered = false; + var lvalues_encountered = false; + var lvalues = Object.create(null); + for (var_defs_index = end_pos; --var_defs_index >= 0;) { + var var_decl = var_defs[var_defs_index]; + // `drop_unused()` shuffles variables without values to the top, + // so we can terminate upon first sighting as an optimization. + if (var_decl.value == null) break; + var var_name = var_decl.name.name; - // Bail if we've seen a var definition of same name before. - if (var_name in var_names_seen) return true; - var_names_seen[var_name] = true; + // Bail if we've seen a var definition of same name before. + if (var_name in var_names_seen) break; + var_names_seen[var_name] = true; - // Only interested in non-constant values. - if (var_decl.value.is_constant()) return; + // Only interested in non-constant values. + if (var_decl.value.is_constant()) continue; - // Only interested in cases with just one reference to the variable. - var def = var_decl.name.definition(); - if (def.references.length !== 1 - || var_name == "arguments" || (!toplevel && def.global)) { - side_effects_encountered = true; - return; - } - var ref = def.references[0]; - - // Don't replace ref if eval() or with statement in scope. - if (ref.scope.uses_eval || ref.scope.uses_with) return true; - - // Restrict var replacement to constants if side effects encountered. - if (side_effects_encountered |= lvalues_encountered) return; - - var value_has_side_effects = var_decl.value.has_side_effects(compressor); - // Non-constant single use vars can only be replaced in same scope. - if (ref.scope !== scope) { - side_effects_encountered |= value_has_side_effects; - return; - } - - // Detect lvalues in var value. - var tw = new TreeWalker(function(node){ - if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) { - lvalues[node.name] = lvalues_encountered = true; + // Only interested in cases with just one reference to the variable. + var def = var_decl.name.definition(); + if (def.references.length !== 1 + || var_name == "arguments" || (!toplevel && def.global)) { + side_effects_encountered = true; + continue; } - }); - var_decl.value.walk(tw); + var ref = def.references[0]; - // Replace the non-constant single use var in statement if side effect free. - var unwind = false; - var tt = new TreeTransformer( - function preorder(node) { - if (unwind || node instanceof AST_Scope && node !== scope) return node; - var parent = tt.parent(); - if (node instanceof AST_Try - || node instanceof AST_With - || node instanceof AST_Case - || node instanceof AST_IterationStatement - || (parent instanceof AST_If && node !== parent.condition) - || (parent instanceof AST_Conditional && node !== parent.condition) - || (node instanceof AST_SymbolRef - && value_has_side_effects - && !are_references_in_scope(node.definition(), scope)) - || (parent instanceof AST_Binary - && (parent.operator == "&&" || parent.operator == "||") - && node === parent.right) - || (parent instanceof AST_Switch && node !== parent.expression)) { - return side_effects_encountered = unwind = true, node; + // Don't replace ref if eval() or with statement in scope. + if (ref.scope.uses_eval || ref.scope.uses_with) break; + + // Restrict var replacement to constants if side effects encountered. + if (side_effects_encountered |= lvalues_encountered) continue; + + var value_has_side_effects = var_decl.value.has_side_effects(compressor); + // Non-constant single use vars can only be replaced in same scope. + if (ref.scope !== scope) { + side_effects_encountered |= value_has_side_effects; + continue; + } + + // Detect lvalues in var value. + var tw = new TreeWalker(function(node){ + if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) { + lvalues[node.name] = lvalues_encountered = true; } - function are_references_in_scope(def, scope) { - if (def.orig.length === 1 - && def.orig[0] instanceof AST_SymbolDefun) return true; - if (def.scope !== scope) return false; - var refs = def.references; - for (var i = 0, len = refs.length; i < len; i++) { - if (refs[i].scope !== scope) return false; + }); + var_decl.value.walk(tw); + + // Replace the non-constant single use var in statement if side effect free. + var unwind = false; + var tt = new TreeTransformer( + function preorder(node) { + if (unwind || node instanceof AST_Scope && node !== scope) return node; + var parent = tt.parent(); + if (node instanceof AST_Try + || node instanceof AST_With + || node instanceof AST_Case + || node instanceof AST_IterationStatement + || (parent instanceof AST_If && node !== parent.condition) + || (parent instanceof AST_Conditional && node !== parent.condition) + || (node instanceof AST_SymbolRef + && value_has_side_effects + && !are_references_in_scope(node.definition(), scope)) + || (parent instanceof AST_Binary + && (parent.operator == "&&" || parent.operator == "||") + && node === parent.right) + || (parent instanceof AST_Switch && node !== parent.expression)) { + return side_effects_encountered = unwind = true, node; + } + function are_references_in_scope(def, scope) { + if (def.orig.length === 1 + && def.orig[0] instanceof AST_SymbolDefun) return true; + if (def.scope !== scope) return false; + var refs = def.references; + for (var i = 0, len = refs.length; i < len; i++) { + if (refs[i].scope !== scope) return false; + } + return true; + } + }, + function postorder(node) { + if (unwind) return node; + if (node === ref) + return unwind = true, replace_var(var_decl, node, tt.parent(), false); + if (side_effects_encountered |= node.has_side_effects(compressor)) + return unwind = true, node; + if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) { + side_effects_encountered = true; + return unwind = true, node; } - return true; } - }, - function postorder(node) { - if (unwind) return node; - if (node === ref) - return unwind = true, replace_var(var_decl, node, tt.parent(), false); - if (side_effects_encountered |= node.has_side_effects(compressor)) - return unwind = true, node; - if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) { - side_effects_encountered = true; - return unwind = true, node; - } - } - ); - stat.transform(tt); + ); + stat.transform(tt); + } } function is_lvalue(node, parent) { @@ -777,6 +781,7 @@ merge(Compressor.prototype, { var_decl.value = null; var_defs.splice(var_defs_index, 1); + def_stat_index--; if (var_defs.length === 0) { statements.splice(prev_stat_index, 1); stat_index--; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 0f82b743..94515a6f 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1654,3 +1654,26 @@ iife_2: { }(bar()); } } + +var_defs: { + 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 + } + input: { + var f1 = function(x, y) { + var a, b, r = x + y, q = r * r, z = q - r, a = z, b = 7; + console.log(a + b); + }; + f1("1", 0); + } + expect: { + var f1 = function(x, y) { + var r = x + y, a = r * r - r, b = 7; + console.log(a + b); + }; + f1("1", 0); + } + expect_stdout: "97" +} From 88e7a542cd8a8406f54c53cefe72944452f5e013 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 20 Apr 2017 04:18:38 +0800 Subject: [PATCH 16/36] fix `unused` on labeled for-loop (#1831) fixes #1830 --- lib/compress.js | 34 ++++++++++++++++++++-------------- test/compress/drop-unused.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 2612d9ab..7fa2b52b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2090,26 +2090,32 @@ merge(Compressor.prototype, { return maintain_this_binding(tt.parent(), node, node.right.transform(tt)); } } + // certain combination of unused name + side effect leads to: + // https://github.com/mishoo/UglifyJS2/issues/44 + // https://github.com/mishoo/UglifyJS2/issues/1830 + // that's an invalid AST. + // We fix it at this stage by moving the `var` outside the `for`. if (node instanceof AST_For) { descend(node, this); - if (node.init instanceof AST_BlockStatement) { - // certain combination of unused name + side effect leads to: - // https://github.com/mishoo/UglifyJS2/issues/44 - // that's an invalid AST. - // We fix it at this stage by moving the `var` outside the `for`. - - var body = node.init.body.slice(0, -1); - node.init = node.init.body.slice(-1)[0].body; - body.push(node); - - return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { - body: body - }); + var block = node.init; + node.init = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; } else if (is_empty(node.init)) { node.init = null; - return node; } + return node; + } + if (node instanceof AST_LabeledStatement && node.body instanceof AST_For) { + descend(node, this); + if (node.body instanceof AST_BlockStatement) { + var block = node.body; + node.body = block.body.pop(); + block.body.push(node); + return in_list ? MAP.splice(block.body) : block; + } + return node; } if (node instanceof AST_Scope && node !== self) return node; diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 2eefbe8d..8f0aa0bf 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1056,3 +1056,38 @@ drop_var: { "3 2", ] } + +issue_1830_1: { + options = { + unused: true, + } + input: { + !function() { + L: for (var b = console.log(1); !1;) continue L; + }(); + } + expect: { + !function() { + L: for (console.log(1); !1;) continue L; + }(); + } + expect_stdout: "1" +} + +issue_1830_2: { + options = { + unused: true, + } + input: { + !function() { + L: for (var a = 1, b = console.log(a); --a;) continue L; + }(); + } + expect: { + !function() { + var a = 1; + L: for (console.log(a); --a;) continue L; + }(); + } + expect_stdout: "1" +} From f05d4f7af3659a481b06449584fa63e3772322e2 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Thu, 20 Apr 2017 13:06:14 +0800 Subject: [PATCH 17/36] improve `unused` (#1832) - extract leading value with side-effects out of `var` statement - reduce scanning of `AST_Definitions` from 3 passes to just once --- lib/compress.js | 166 +++++++++++++++------------------ test/compress/collapse_vars.js | 6 +- test/compress/drop-unused.js | 3 +- test/compress/reduce_vars.js | 12 +-- 4 files changed, 82 insertions(+), 105 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 7fa2b52b..772c6227 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1955,12 +1955,7 @@ merge(Compressor.prototype, { sym.__unused = true; if (trim) { a.pop(); - compressor[sym.unreferenced() ? "warn" : "info"]("Dropping unused function argument {name} [{file}:{line},{col}]", { - name : sym.name, - file : sym.start.file, - line : sym.start.line, - col : sym.start.col - }); + compressor[sym.unreferenced() ? "warn" : "info"]("Dropping unused function argument {name} [{file}:{line},{col}]", template(sym)); } } else { @@ -1970,115 +1965,93 @@ merge(Compressor.prototype, { } if (drop_funcs && node instanceof AST_Defun && node !== self) { if (!(node.name.definition().id in in_use_ids)) { - compressor[node.name.unreferenced() ? "warn" : "info"]("Dropping unused function {name} [{file}:{line},{col}]", { - name : node.name.name, - file : node.name.start.file, - line : node.name.start.line, - col : node.name.start.col - }); + compressor[node.name.unreferenced() ? "warn" : "info"]("Dropping unused function {name} [{file}:{line},{col}]", template(node.name)); return make_node(AST_EmptyStatement, node); } return node; } if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { - var def = node.definitions.filter(function(def){ - var w = { - name : def.name.name, - file : def.name.start.file, - line : def.name.start.line, - col : def.name.start.col - }; + // place uninitialized names at the start + var body = [], head = [], tail = []; + // for unused names whose initialization has + // side effects, we can cascade the init. code + // into the next one, or next statement. + var side_effects = []; + node.definitions.forEach(function(def) { if (def.value) def.value = def.value.transform(tt); var sym = def.name.definition(); if (sym.id in in_use_ids) { if (def.name instanceof AST_SymbolVar) { var var_defs = var_defs_by_id.get(sym.id); if (var_defs.length > 1 && !def.value) { - compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", w); + compressor.warn("Dropping duplicated definition of variable {name} [{file}:{line},{col}]", template(def.name)); var_defs.splice(var_defs.indexOf(def), 1); - return false; + return; } } - return true; + if (def.value) { + if (side_effects.length > 0) { + if (tail.length > 0) { + merge_sequence(side_effects, def.value); + def.value = make_sequence(def.value, side_effects); + } else { + body.push(make_node(AST_SimpleStatement, node, { + body: make_sequence(node, side_effects) + })); + } + side_effects = []; + } + tail.push(def); + } else { + head.push(def); + } + } else if (sym.orig[0] instanceof AST_SymbolCatch) { + var value = def.value && def.value.drop_side_effect_free(compressor); + if (value) merge_sequence(side_effects, value); + def.value = null; + head.push(def); + } else { + var value = def.value && def.value.drop_side_effect_free(compressor); + if (value) { + compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", template(def.name)); + merge_sequence(side_effects, value); + } else { + compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", template(def.name)); + } } - if (sym.orig[0] instanceof AST_SymbolCatch) { - def.value = def.value && def.value.drop_side_effect_free(compressor); - return true; - } - if (def.value && (def._unused_side_effects = def.value.drop_side_effect_free(compressor))) { - compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", w); - return true; - } - compressor[def.name.unreferenced() ? "warn" : "info"]("Dropping unused variable {name} [{file}:{line},{col}]", w); - return false; }); - if (def.length == 1 - && def[0].value - && !def[0]._unused_side_effects - && def[0].name instanceof AST_SymbolVar) { - var var_defs = var_defs_by_id.get(def[0].name.definition().id); + if (head.length == 0 && tail.length == 1 && tail[0].name instanceof AST_SymbolVar) { + var var_defs = var_defs_by_id.get(tail[0].name.definition().id); if (var_defs.length > 1) { - compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", { - name : def[0].name.name, - file : def[0].name.start.file, - line : def[0].name.start.line, - col : def[0].name.start.col - }); - var_defs.splice(var_defs.indexOf(def[0]), 1); - return make_node(AST_SimpleStatement, node, { - body: make_node(AST_Assign, def[0], { - operator: "=", - left: make_node(AST_SymbolRef, def[0].name, def[0].name), - right: def[0].value - }) - }); + var def = tail.pop(); + compressor.warn("Converting duplicated definition of variable {name} to assignment [{file}:{line},{col}]", template(def.name)); + var_defs.splice(var_defs.indexOf(def), 1); + side_effects.unshift(make_node(AST_Assign, def, { + operator: "=", + left: make_node(AST_SymbolRef, def.name, def.name), + right: def.value + })); } } - // place uninitialized names at the start - def = mergeSort(def, function(a, b){ - if (!a.value && b.value) return -1; - if (!b.value && a.value) return 1; - return 0; - }); - // for unused names whose initialization has - // side effects, we can cascade the init. code - // into the next one, or next statement. - var side_effects = []; - for (var i = 0; i < def.length;) { - var x = def[i]; - if (x._unused_side_effects) { - merge_sequence(side_effects, x._unused_side_effects); - def.splice(i, 1); - } else { - if (side_effects.length > 0) { - merge_sequence(side_effects, x.value); - x.value = make_sequence(x.value, side_effects); - side_effects = []; - } - ++i; - } + if (head.length > 0 || tail.length > 0) { + node.definitions = head.concat(tail); + body.push(node); } if (side_effects.length > 0) { - side_effects = make_node(AST_BlockStatement, node, { - body: [ make_node(AST_SimpleStatement, node, { - body: make_sequence(node, side_effects) - }) ] + body.push(make_node(AST_SimpleStatement, node, { + body: make_sequence(node, side_effects) + })); + } + switch (body.length) { + case 0: + return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); + case 1: + return body[0]; + default: + return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { + body: body }); - } else { - side_effects = null; } - if (def.length == 0 && !side_effects) { - return make_node(AST_EmptyStatement, node); - } - if (def.length == 0) { - return in_list ? MAP.splice(side_effects.body) : side_effects; - } - node.definitions = def; - if (side_effects) { - side_effects.body.unshift(node); - return in_list ? MAP.splice(side_effects.body) : side_effects; - } - return node; } if (drop_vars && assign_as_unused && node instanceof AST_Assign @@ -2119,6 +2092,15 @@ merge(Compressor.prototype, { } if (node instanceof AST_Scope && node !== self) return node; + + function template(sym) { + return { + name : sym.name, + file : sym.start.file, + line : sym.start.line, + col : sym.start.col + }; + } } ); self.transform(tt); diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 94515a6f..a4c1f9e6 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -701,10 +701,10 @@ collapse_vars_lvalues_drop_assign: { function f2(x) { var z = x, a = ++z; return z += a; } function f3(x) { var a = (x -= 3); return x + a; } function f4(x) { var a = (x -= 3); return x + a; } - function f5(x) { var v = (e1(), e2()), c = v = --x; return x - c; } + function f5(x) { e1(); var v = e2(), c = v = --x; return x - c; } function f6(x) { e1(), e2(); return --x - x; } - function f7(x) { var c = (e1(), e2() - x); return x - c; } - function f8(x) { var v = (e1(), e2()); return x - (v - x); } + function f7(x) { e1(); var c = e2() - x; return x - c; } + function f8(x) { e1(); var v = e2(); return x - (v - x); } function f9(x) { e1(); return e2() - x - x; } } } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 8f0aa0bf..7456f676 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -935,7 +935,8 @@ issue_1715_3: { try { console; } catch (a) { - var a = x(); + var a; + x(); } } f(); diff --git a/test/compress/reduce_vars.js b/test/compress/reduce_vars.js index c5f26904..57e23891 100644 --- a/test/compress/reduce_vars.js +++ b/test/compress/reduce_vars.js @@ -53,9 +53,7 @@ reduce_vars: { console.log(-3); eval("console.log(a);"); })(eval); - (function() { - return "yes"; - })(); + "yes"; console.log(2); } expect_stdout: true @@ -1699,9 +1697,7 @@ redefine_arguments_2: { console.log(function() { var arguments; return typeof arguments; - }(), function() { - return"number"; - }(), function(x) { + }(), "number", function(x) { var arguments = x; return typeof arguments; }()); @@ -1810,9 +1806,7 @@ redefine_farg_2: { console.log(function(a) { var a; return typeof a; - }([]), function() { - return "number"; - }(),function(a, b) { + }([]), "number",function(a, b) { var a = b; return typeof a; }([])); From 6f954aa3d0a505791753c6cb6273e98d84895915 Mon Sep 17 00:00:00 2001 From: Roman Dvornov Date: Thu, 20 Apr 2017 21:23:41 +0300 Subject: [PATCH 18/36] Fix API reference examples (#1834) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 672c2f3b..ff3ba7bd 100644 --- a/README.md +++ b/README.md @@ -631,9 +631,10 @@ be appended to code. You can also specify sourceRoot property to be included in source map: ```javascript var result = UglifyJS.minify({"file1.js": "var a = function() {};"}, { - sourceMap: { + sourceMap: { root: "http://example.com/src", url: "out.js.map" + } }); ``` @@ -641,9 +642,9 @@ If you're compressing compiled JavaScript and have a source map for it, you can use `sourceMap.content`: ```javascript var result = UglifyJS.minify({"compiled.js": "compiled code"}, { - sourceMap: { + sourceMap: { content: "content from compiled.js.map", - url: "minified.js.map" + url: "minified.js.map" } }); // same as before, it returns `code` and `map` From ca32a09032b3e7d6aac1f0b01f67a0b67b3037f1 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sat, 22 Apr 2017 22:15:04 +0800 Subject: [PATCH 19/36] fix label-related bugs (#1835) - deep cloning of `AST_LabeledStatement` - `L:do{...}while(false)` - empty statement with label within block extend `test/ufuzz.js` - generate labels for blocks & loops - generate for-in statements - skip suspicious option search if `minify()` errs fixes #1833 --- lib/ast.js | 9 +- lib/compress.js | 11 +-- lib/output.js | 10 +-- test/compress/issue-1833.js | 134 ++++++++++++++++++++++++++++++ test/ufuzz.js | 159 ++++++++++++++++++++++-------------- 5 files changed, 244 insertions(+), 79 deletions(-) create mode 100644 test/compress/issue-1833.js diff --git a/lib/ast.js b/lib/ast.js index 739c21c2..e61a31e5 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -206,12 +206,13 @@ var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { clone: function(deep) { var node = this._clone(deep); if (deep) { - var refs = node.label.references; - var label = this.label; + var label = node.label; + var def = this.label; node.walk(new TreeWalker(function(node) { if (node instanceof AST_LoopControl - && node.label && node.label.thedef === label) { - refs.push(node); + && node.label && node.label.thedef === def) { + node.label.thedef = label; + label.references.push(node); } })); } diff --git a/lib/compress.js b/lib/compress.js index 772c6227..22e012e9 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2395,7 +2395,7 @@ merge(Compressor.prototype, { 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 }); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } if (self instanceof AST_Do) { var has_loop_control = false; @@ -2404,7 +2404,8 @@ merge(Compressor.prototype, { if (node instanceof AST_LoopControl && tw.loopcontrol_target(node) === self) return has_loop_control = true; }); - self.walk(tw); + var parent = compressor.parent(); + (parent instanceof AST_LabeledStatement ? parent : self).walk(tw); if (!has_loop_control) return self.body; } } @@ -2474,7 +2475,7 @@ merge(Compressor.prototype, { })); } extract_declarations_from_unreachable_code(compressor, self.body, a); - return make_node(AST_BlockStatement, self, { body: a }); + return make_node(AST_BlockStatement, self, { body: a }).optimize(compressor); } if (cond !== self.condition) { cond = make_node_from_constant(cond, self.condition).transform(compressor); @@ -2726,9 +2727,9 @@ merge(Compressor.prototype, { var body = []; if (self.bcatch) extract_declarations_from_unreachable_code(compressor, self.bcatch, body); if (self.bfinally) body = body.concat(self.bfinally.body); - return body.length > 0 ? make_node(AST_BlockStatement, self, { + return make_node(AST_BlockStatement, self, { body: body - }).optimize(compressor) : make_node(AST_EmptyStatement, self); + }).optimize(compressor); } return self; }); diff --git a/lib/output.js b/lib/output.js index 7a2e850f..33f4c533 100644 --- a/lib/output.js +++ b/lib/output.js @@ -190,11 +190,7 @@ function OutputStream(options) { var might_need_space = false; var might_need_semicolon = false; var might_add_newline = 0; - var last = null; - - function last_char() { - return last.charAt(last.length - 1); - }; + var last = ""; var ensure_line_len = options.max_line_len ? function() { if (current_col > options.max_line_len) { @@ -218,10 +214,11 @@ function OutputStream(options) { function print(str) { str = String(str); var ch = str.charAt(0); + var prev = last.charAt(last.length - 1); if (might_need_semicolon) { might_need_semicolon = false; - if ((!ch || ";}".indexOf(ch) < 0) && !/[;]$/.test(last)) { + if (prev == ":" && ch == "}" || (!ch || ";}".indexOf(ch) < 0) && prev != ";") { if (options.semicolons || requireSemicolonChars(ch)) { OUTPUT += ";"; current_col++; @@ -258,7 +255,6 @@ function OutputStream(options) { } if (might_need_space) { - var prev = last_char(); if ((is_identifier_char(prev) && (is_identifier_char(ch) || ch == "\\")) || (ch == "/" && ch == prev) diff --git a/test/compress/issue-1833.js b/test/compress/issue-1833.js new file mode 100644 index 00000000..e46dd046 --- /dev/null +++ b/test/compress/issue-1833.js @@ -0,0 +1,134 @@ +iife_for: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: for (;;) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: for (;;) break L; + }(); + }(); + } +} + +iife_for_in: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: for (var a in x) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: for (var a in x) break L; + }(); + }(); + } +} + +iife_do: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: do { + break L; + } while (1); + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: do { + break L; + } while (1); + }(); + }(); + } +} + +iife_while: { + options = { + negate_iife: true, + reduce_vars: true, + toplevel: true, + unused: true, + } + input: { + function f() { + function g() { + L: while (1) break L; + } + g(); + } + f(); + } + expect: { + !function() { + !function() { + L: while (1) break L; + }(); + }(); + } +} + +label_do: { + options = { + evaluate: true, + loops: true, + } + input: { + L: do { + continue L; + } while (0); + } + expect: { + L: do { + continue L; + } while (0); + } +} + +label_while: { + options = { + evaluate: true, + dead_code: true, + loops: true, + } + input: { + function f() { + L: while (0) continue L; + } + } + expect_exact: "function f(){L:;}" +} diff --git a/test/ufuzz.js b/test/ufuzz.js index 67d65045..a542d145 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -20,49 +20,26 @@ var MAX_GENERATED_TOPLEVELS_PER_RUN = 1; var MAX_GENERATION_RECURSION_DEPTH = 12; var INTERVAL_COUNT = 100; -var STMT_BLOCK = 0; -var STMT_IF_ELSE = 1; -var STMT_DO_WHILE = 2; -var STMT_WHILE = 3; -var STMT_FOR_LOOP = 4; -var STMT_SEMI = 5; -var STMT_EXPR = 6; -var STMT_SWITCH = 7; -var STMT_VAR = 8; -var STMT_RETURN_ETC = 9; -var STMT_FUNC_EXPR = 10; -var STMT_TRY = 11; -var STMT_C = 12; -var STMTS_TO_USE = [ - STMT_BLOCK, - STMT_IF_ELSE, - STMT_DO_WHILE, - STMT_WHILE, - STMT_FOR_LOOP, - STMT_SEMI, - STMT_EXPR, - STMT_SWITCH, - STMT_VAR, - STMT_RETURN_ETC, - STMT_FUNC_EXPR, - STMT_TRY, - STMT_C, -]; -var STMT_ARG_TO_ID = { - block: STMT_BLOCK, - ifelse: STMT_IF_ELSE, - dowhile: STMT_DO_WHILE, - while: STMT_WHILE, - forloop: STMT_FOR_LOOP, - semi: STMT_SEMI, - expr: STMT_EXPR, - switch: STMT_SWITCH, - var: STMT_VAR, - stop: STMT_RETURN_ETC, - funcexpr: STMT_FUNC_EXPR, - try: STMT_TRY, - c: STMT_C, -}; +var STMT_ARG_TO_ID = Object.create(null); +var STMTS_TO_USE = []; +function STMT_(name) { + return STMT_ARG_TO_ID[name] = STMTS_TO_USE.push(STMTS_TO_USE.length) - 1; +} + +var STMT_BLOCK = STMT_("block"); +var STMT_IF_ELSE = STMT_("ifelse"); +var STMT_DO_WHILE = STMT_("dowhile"); +var STMT_WHILE = STMT_("while"); +var STMT_FOR_LOOP = STMT_("forloop"); +var STMT_FOR_IN = STMT_("forin"); +var STMT_SEMI = STMT_("semi"); +var STMT_EXPR = STMT_("expr"); +var STMT_SWITCH = STMT_("switch"); +var STMT_VAR = STMT_("var"); +var STMT_RETURN_ETC = STMT_("stop"); +var STMT_FUNC_EXPR = STMT_("funcexpr"); +var STMT_TRY = STMT_("try"); +var STMT_C = STMT_("c"); var STMT_FIRST_LEVEL_OVERRIDE = -1; var STMT_SECOND_LEVEL_OVERRIDE = -1; @@ -296,6 +273,7 @@ var TYPEOF_OUTCOMES = [ var loops = 0; var funcs = 0; +var labels = 10000; function rng(max) { var r = randomBytes(2).readUInt16LE(0) / 65536; @@ -345,7 +323,7 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { s = 'function ' + name + '(' + createParams() + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; } else { // functions with statements - s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_THROW, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; + s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; } VAR_NAMES.length = namesLenBefore; @@ -367,6 +345,40 @@ function createStatements(n, recurmax, canThrow, canBreak, canContinue, cannotRe return s; } +function enableLoopControl(flag, defaultValue) { + return Array.isArray(flag) && flag.indexOf("") < 0 ? flag.concat("") : flag || defaultValue; +} + +function createLabel(canBreak, canContinue) { + var label; + if (rng(10) < 3) { + label = ++labels; + if (Array.isArray(canBreak)) { + canBreak = canBreak.slice(); + } else { + canBreak = canBreak ? [ "" ] : []; + } + canBreak.push(label); + if (Array.isArray(canContinue)) { + canContinue = canContinue.slice(); + } else { + canContinue = canContinue ? [ "" ] : []; + } + canContinue.push(label); + } + return { + break: canBreak, + continue: canContinue, + target: label ? "L" + label + ": " : "" + }; +} + +function getLabel(label) { + if (!Array.isArray(label)) return ""; + label = label[rng(label.length)]; + return label && " L" + label; +} + function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { ++stmtDepth; var loop = ++loops; @@ -382,15 +394,34 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn switch (target) { case STMT_BLOCK: - return '{' + createStatements(rng(5) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}'; + var label = createLabel(canBreak); + return label.target + '{' + createStatements(rng(5) + 1, recurmax, canThrow, label.break, canContinue, cannotReturn, stmtDepth) + '}'; case STMT_IF_ELSE: return 'if (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + (rng(2) === 1 ? ' else ' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) : ''); case STMT_DO_WHILE: - return '{var brake' + loop + ' = 5; do {' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth) + '} while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0);}'; + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return '{var brake' + loop + ' = 5; ' + label.target + 'do {' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '} while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0);}'; case STMT_WHILE: - return '{var brake' + loop + ' = 5; while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth) + '}'; + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return '{var brake' + loop + ' = 5; ' + label.target + 'while ((' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && --brake' + loop + ' > 0)' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}'; case STMT_FOR_LOOP: - return 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, CAN_BREAK, CAN_CONTINUE, cannotReturn, stmtDepth); + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + return label.target + 'for (var brake' + loop + ' = 5; (' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ') && brake' + loop + ' > 0; --brake' + loop + ')' + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth); + case STMT_FOR_IN: + var label = createLabel(canBreak, canContinue); + canBreak = label.break || enableLoopControl(canBreak, CAN_BREAK); + canContinue = label.continue || enableLoopControl(canContinue, CAN_CONTINUE); + var optElementVar = ''; + if (rng(5) > 1) { + optElementVar = 'c = 1 + c; var ' + createVarName(MANDATORY) + ' = expr' + loop + '[key' + loop + ']; '; + } + return '{var expr' + loop + ' = ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '; ' + label.target + ' for (var key' + loop + ' in expr' + loop + ') {' + optElementVar + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}}'; case STMT_SEMI: return ';'; case STMT_EXPR: @@ -424,8 +455,8 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn case 1: case 2: case 3: - if (canBreak && rng(5) === 0) return 'break;'; - if (canContinue && rng(5) === 0) return 'continue;'; + if (canBreak && rng(5) === 0) return 'break' + getLabel(canBreak) + ';'; + if (canContinue && rng(5) === 0) return 'continue' + getLabel(canContinue) + ';'; if (cannotReturn) return createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; if (rng(3) == 0) return '/*3*/return;'; return 'return ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ';'; @@ -470,25 +501,27 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn function createSwitchParts(recurmax, n, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) { var hadDefault = false; - var s = ''; + var s = ['']; + canBreak = enableLoopControl(canBreak, CAN_BREAK); while (n-- > 0) { //hadDefault = n > 0; // disables weird `default` clause positioning (use when handling destabilizes) if (hadDefault || rng(5) > 0) { - s += '' + - 'case ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':\n' + - createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + - '\n' + - (rng(10) > 0 ? ' break;' : '/* fall-through */') + - '\n'; + s.push( + 'case ' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':', + createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), + rng(10) > 0 ? ' break;' : '/* fall-through */', + '' + ); } else { hadDefault = true; - s += '' + - 'default:\n' + - createStatements(rng(3) + 1, recurmax, canThrow, CAN_BREAK, canContinue, cannotReturn, stmtDepth) + - '\n'; + s.push( + 'default:', + createStatements(rng(3) + 1, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth), + '' + ); } } - return s; + return s.join('\n'); } function createExpression(recurmax, noComma, stmtDepth, canThrow) { @@ -862,7 +895,7 @@ function log(options) { options = JSON.parse(options); console.log(options); console.log(); - if (!ok) { + if (!ok && typeof uglify_code == "string") { Object.keys(default_options).forEach(log_suspects.bind(null, options)); console.log("!!!!!! Failed... round", round); } From 45ce3694807ee0daeb7e0f84c12ffbd8ca4f733c Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 23 Apr 2017 01:51:56 +0800 Subject: [PATCH 20/36] fix `AST_For.init` patch-up in `drop_unused()` (#1839) fixes #1838 --- lib/compress.js | 3 +++ test/compress/drop-unused.js | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 22e012e9..5a8f23bf 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2066,6 +2066,7 @@ merge(Compressor.prototype, { // certain combination of unused name + side effect leads to: // https://github.com/mishoo/UglifyJS2/issues/44 // https://github.com/mishoo/UglifyJS2/issues/1830 + // https://github.com/mishoo/UglifyJS2/issues/1838 // that's an invalid AST. // We fix it at this stage by moving the `var` outside the `for`. if (node instanceof AST_For) { @@ -2075,6 +2076,8 @@ merge(Compressor.prototype, { node.init = block.body.pop(); block.body.push(node); return in_list ? MAP.splice(block.body) : block; + } else if (node.init instanceof AST_SimpleStatement) { + node.init = node.init.body; } else if (is_empty(node.init)) { node.init = null; } diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 7456f676..c702cfaf 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -1092,3 +1092,25 @@ issue_1830_2: { } expect_stdout: "1" } + +issue_1838: { + options = { + join_vars: true, + loops: true, + unused: true, + } + beautify = { + beautify: true, + } + input: { + function f() { + var b = a; + while (c); + } + } + expect_exact: [ + "function f() {", + " for (a; c; ) ;", + "}", + ] +} From 64d74432f6e475df921d0ec49c4d15e5d2ae891d Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 22 Apr 2017 16:28:32 -0400 Subject: [PATCH 21/36] update README for 3.x (#1840) --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff3ba7bd..24fe441c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -UglifyJS -======== +UglifyJS 3 +========== [![Build Status](https://travis-ci.org/mishoo/UglifyJS2.svg)](https://travis-ci.org/mishoo/UglifyJS2) UglifyJS is a JavaScript parser, minifier, compressor or beautifier toolkit. #### Note: -- `uglify-js 3.x` is incompatible with the [`2.x` branch](https://github.com/mishoo/UglifyJS2/tree/v2.x). -- release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify +- **`uglify-js@3.x` has a new API and CLI and is not backwards compatible with [`uglify-js@2.x`](https://github.com/mishoo/UglifyJS2/tree/v2.x)**. +- **Documentation for UglifyJS `2.x` releases can be found [here](https://github.com/mishoo/UglifyJS2/tree/v2.x)**. +- Release versions of `uglify-js` only support ECMAScript 5 (ES5). If you wish to minify ES2015+ (ES6+) code then please use the [harmony](#harmony) development branch. - Node 7 has a known performance regression and runs `uglify-js` twice as slow. From 9bf72cf75822044ae314b4646db9aefb1bd38284 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 23 Apr 2017 20:05:22 +0800 Subject: [PATCH 22/36] improve parser under "use strict" (#1836) - `const` without value - `delete` of expression - redefining `arguments` or `eval` extend `test/ufuzz.js` - optionally generate "use strict" - improve handling of test cases with syntax errors - group IIFE generation - generate bare anonymous functions - workaround `console.log()` for `new function()` - generate expressions with `this` fixes #1810 --- lib/parse.js | 74 +++++++++++------- test/input/invalid/const.js | 8 ++ test/input/invalid/delete.js | 14 ++++ test/input/invalid/function_1.js | 6 ++ test/input/invalid/function_2.js | 6 ++ test/input/invalid/function_3.js | 6 ++ test/input/invalid/try.js | 8 ++ test/input/invalid/var.js | 8 ++ test/mocha/cli.js | 105 +++++++++++++++++++++++++ test/sandbox.js | 38 ++++++--- test/ufuzz.js | 129 +++++++++++++++++++++++-------- 11 files changed, 333 insertions(+), 69 deletions(-) create mode 100644 test/input/invalid/const.js create mode 100644 test/input/invalid/delete.js create mode 100644 test/input/invalid/function_1.js create mode 100644 test/input/invalid/function_2.js create mode 100644 test/input/invalid/function_3.js create mode 100644 test/input/invalid/try.js create mode 100644 test/input/invalid/var.js diff --git a/lib/parse.js b/lib/parse.js index 40528df1..3493f8e6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -629,8 +629,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { } next_token.has_directive = function(directive) { - return S.directives[directive] !== undefined && - S.directives[directive] > 0; + return S.directives[directive] > 0; } return next_token; @@ -1033,29 +1032,32 @@ function parse($TEXT, options) { if (in_statement && !name) unexpected(); expect("("); + var argnames = []; + for (var first = true; !is("punc", ")");) { + if (first) first = false; else expect(","); + argnames.push(as_symbol(AST_SymbolFunarg)); + } + next(); + var loop = S.in_loop; + var labels = S.labels; + ++S.in_function; + S.in_directives = true; + S.input.push_directives_stack(); + S.in_loop = 0; + S.labels = []; + var body = block_(); + if (S.input.has_directive("use strict")) { + if (name) strict_verify_symbol(name); + argnames.forEach(strict_verify_symbol); + } + S.input.pop_directives_stack(); + --S.in_function; + S.in_loop = loop; + S.labels = labels; return new ctor({ name: name, - argnames: (function(first, a){ - while (!is("punc", ")")) { - if (first) first = false; else expect(","); - a.push(as_symbol(AST_SymbolFunarg)); - } - next(); - return a; - })(true, []), - body: (function(loop, labels){ - ++S.in_function; - S.in_directives = true; - S.input.push_directives_stack(); - S.in_loop = 0; - S.labels = []; - var a = block_(); - S.input.pop_directives_stack(); - --S.in_function; - S.in_loop = loop; - S.labels = labels; - return a; - })(S.in_loop, S.labels) + argnames: argnames, + body: body }); }; @@ -1157,7 +1159,10 @@ function parse($TEXT, options) { a.push(new AST_VarDef({ start : S.token, name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), - value : is("operator", "=") ? (next(), expression(false, no_in)) : null, + value : is("operator", "=") + ? (next(), expression(false, no_in)) + : in_const && S.input.has_directive("use strict") + ? croak("Missing initializer in const declaration") : null, end : prev() })); if (!is("punc", ",")) @@ -1384,12 +1389,20 @@ function parse($TEXT, options) { }); }; + function strict_verify_symbol(sym) { + if (sym.name == "arguments" || sym.name == "eval") + croak("Unexpected " + sym.name + " in strict mode", sym.start.line, sym.start.col, sym.start.pos); + } + function as_symbol(type, noerror) { if (!is("name")) { if (!noerror) croak("Name expected"); return null; } var sym = _make_symbol(type); + if (S.input.has_directive("use strict") && sym instanceof AST_SymbolDeclaration) { + strict_verify_symbol(sym); + } next(); return sym; }; @@ -1450,8 +1463,17 @@ function parse($TEXT, options) { function make_unary(ctor, token, expr) { var op = token.value; - if ((op == "++" || op == "--") && !is_assignable(expr)) - croak("Invalid use of " + op + " operator", token.line, token.col, token.pos); + switch (op) { + case "++": + case "--": + if (!is_assignable(expr)) + croak("Invalid use of " + op + " operator", token.line, token.col, token.pos); + break; + case "delete": + if (expr instanceof AST_SymbolRef && S.input.has_directive("use strict")) + croak("Calling delete on expression not allowed in strict mode", expr.start.line, expr.start.col, expr.start.pos); + break; + } return new ctor({ operator: op, expression: expr }); }; diff --git a/test/input/invalid/const.js b/test/input/invalid/const.js new file mode 100644 index 00000000..7a2bfd3d --- /dev/null +++ b/test/input/invalid/const.js @@ -0,0 +1,8 @@ +function f() { + const a; +} + +function g() { + "use strict"; + const a; +} diff --git a/test/input/invalid/delete.js b/test/input/invalid/delete.js new file mode 100644 index 00000000..9753d3af --- /dev/null +++ b/test/input/invalid/delete.js @@ -0,0 +1,14 @@ +function f(x) { + delete 42; + delete (0, x); + delete null; + delete x; +} + +function g(x) { + "use strict"; + delete 42; + delete (0, x); + delete null; + delete x; +} diff --git a/test/input/invalid/function_1.js b/test/input/invalid/function_1.js new file mode 100644 index 00000000..bff9c75a --- /dev/null +++ b/test/input/invalid/function_1.js @@ -0,0 +1,6 @@ +function f(arguments) { +} + +function g(arguments) { + "use strict"; +} diff --git a/test/input/invalid/function_2.js b/test/input/invalid/function_2.js new file mode 100644 index 00000000..cc496a4e --- /dev/null +++ b/test/input/invalid/function_2.js @@ -0,0 +1,6 @@ +function arguments() { +} + +function eval() { + "use strict"; +} diff --git a/test/input/invalid/function_3.js b/test/input/invalid/function_3.js new file mode 100644 index 00000000..4a20d2a6 --- /dev/null +++ b/test/input/invalid/function_3.js @@ -0,0 +1,6 @@ +!function eval() { +}(); + +!function arguments() { + "use strict"; +}(); diff --git a/test/input/invalid/try.js b/test/input/invalid/try.js new file mode 100644 index 00000000..e65a55cc --- /dev/null +++ b/test/input/invalid/try.js @@ -0,0 +1,8 @@ +function f() { + try {} catch (eval) {} +} + +function g() { + "use strict"; + try {} catch (eval) {} +} diff --git a/test/input/invalid/var.js b/test/input/invalid/var.js new file mode 100644 index 00000000..e3ccbe87 --- /dev/null +++ b/test/input/invalid/var.js @@ -0,0 +1,8 @@ +function f() { + var eval; +} + +function g() { + "use strict"; + var eval; +} diff --git a/test/mocha/cli.js b/test/mocha/cli.js index 697c09a3..9d8d496f 100644 --- a/test/mocha/cli.js +++ b/test/mocha/cli.js @@ -379,6 +379,111 @@ describe("bin/uglifyjs", function () { done(); }); }); + it("Should throw syntax error (const a)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/const.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/const.js:7,11", + " const a;", + " ^", + "ERROR: Missing initializer in const declaration" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (delete x)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/delete.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/delete.js:13,11", + " delete x;", + " ^", + "ERROR: Calling delete on expression not allowed in strict mode" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (function g(arguments))", function(done) { + var command = uglifyjscmd + ' test/input/invalid/function_1.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/function_1.js:4,11", + "function g(arguments) {", + " ^", + "ERROR: Unexpected arguments in strict mode" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (function eval())", function(done) { + var command = uglifyjscmd + ' test/input/invalid/function_2.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/function_2.js:4,9", + "function eval() {", + " ^", + "ERROR: Unexpected eval in strict mode" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (iife arguments())", function(done) { + var command = uglifyjscmd + ' test/input/invalid/function_3.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/function_3.js:4,10", + "!function arguments() {", + " ^", + "ERROR: Unexpected arguments in strict mode" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (catch(eval))", function(done) { + var command = uglifyjscmd + ' test/input/invalid/try.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/try.js:7,18", + " try {} catch (eval) {}", + " ^", + "ERROR: Unexpected eval in strict mode" + ].join("\n")); + done(); + }); + }); + it("Should throw syntax error (var eval)", function(done) { + var command = uglifyjscmd + ' test/input/invalid/var.js'; + + exec(command, function (err, stdout, stderr) { + assert.ok(err); + assert.strictEqual(stdout, ""); + assert.strictEqual(stderr.split(/\n/).slice(0, 4).join("\n"), [ + "Parse error at test/input/invalid/var.js:7,8", + " var eval;", + " ^", + "ERROR: Unexpected eval in strict mode" + ].join("\n")); + done(); + }); + }); it("Should handle literal string as source map input", function(done) { var command = [ uglifyjscmd, diff --git a/test/sandbox.js b/test/sandbox.js index 894349fb..eb9f1f0f 100644 --- a/test/sandbox.js +++ b/test/sandbox.js @@ -1,15 +1,35 @@ var vm = require("vm"); +function safe_log(arg) { + if (arg) switch (typeof arg) { + case "function": + return arg.toString(); + case "object": + if (/Error$/.test(arg.name)) return arg.toString(); + arg.constructor.toString(); + for (var key in arg) { + arg[key] = safe_log(arg[key]); + } + } + return arg; +} + var FUNC_TOSTRING = [ "Function.prototype.toString = Function.prototype.valueOf = function() {", - " var ids = [];", + " var id = 0;", " return function() {", - " var i = ids.indexOf(this);", - " if (i < 0) {", - " i = ids.length;", - " ids.push(this);", + ' if (this === Array) return "[Function: Array]";', + ' if (this === Object) return "[Function: Object]";', + " var i = this.name;", + ' if (typeof i != "number") {', + " i = ++id;", + ' Object.defineProperty(this, "name", {', + " get: function() {", + " return i;", + " }", + " });", " }", - ' return "[Function: __func_" + i + "__]";', + ' return "[Function: " + i + "]";', " }", "}();", ].join("\n"); @@ -21,16 +41,14 @@ exports.run_code = function(code) { }; try { vm.runInNewContext([ - "!function() {", FUNC_TOSTRING, + "!function() {", code, "}();", ].join("\n"), { console: { log: function() { - return console.log.apply(console, [].map.call(arguments, function(arg) { - return typeof arg == "function" || arg && /Error$/.test(arg.name) ? arg.toString() : arg; - })); + return console.log.apply(console, [].map.call(arguments, safe_log)); } } }, { timeout: 5000 }); diff --git a/test/ufuzz.js b/test/ufuzz.js index a542d145..12c62651 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -49,6 +49,7 @@ var num_iterations = +process.argv[2] || 1/0; var verbose = false; // log every generated test var verbose_interval = false; // log every 100 generated tests var verbose_error = false; +var use_strict = false; for (var i = 2; i < process.argv.length; ++i) { switch (process.argv[i]) { case '-v': @@ -78,6 +79,9 @@ for (var i = 2; i < process.argv.length; ++i) { STMT_SECOND_LEVEL_OVERRIDE = STMT_ARG_TO_ID[name]; if (!(STMT_SECOND_LEVEL_OVERRIDE >= 0)) throw new Error('Unknown statement name; use -? to get a list'); break; + case '--use-strict': + use_strict = true; + break; case '--stmt-depth-from-func': STMT_COUNT_FROM_GLOBAL = false; break; @@ -104,6 +108,7 @@ for (var i = 2; i < process.argv.length; ++i) { console.log('-r : maximum recursion depth for generator (higher takes longer)'); console.log('-s1 : force the first level statement to be this one (see list below)'); console.log('-s2 : force the second level statement to be this one (see list below)'); + console.log('--use-strict: generate "use strict"'); console.log('--stmt-depth-from-func: reset statement depth counter at each function, counts from global otherwise'); console.log('--only-stmt : a comma delimited white list of statements that may be generated'); console.log('--without-stmt : a comma delimited black list of statements never to generate'); @@ -280,9 +285,19 @@ function rng(max) { return Math.floor(max * r); } +function strictMode() { + return use_strict && rng(4) == 0 ? '"use strict";' : ''; +} + function createTopLevelCode() { - if (rng(2) === 0) return createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0); - return createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0); + return [ + strictMode(), + 'var a = 100, b = 10, c = 0;', + rng(2) == 0 + ? createStatements(3, MAX_GENERATION_RECURSION_DEPTH, CANNOT_THROW, CANNOT_BREAK, CANNOT_CONTINUE, CANNOT_RETURN, 0) + : createFunctions(rng(MAX_GENERATED_TOPLEVELS_PER_RUN) + 1, MAX_GENERATION_RECURSION_DEPTH, IN_GLOBAL, ANY_TYPE, CANNOT_THROW, 0), + 'console.log(null, a, b, c);' // preceding `null` makes for a cleaner output (empty string still shows up etc) + ].join('\n'); } function createFunctions(n, recurmax, inGlobal, noDecl, canThrow, stmtDepth) { @@ -320,10 +335,22 @@ function createFunction(recurmax, inGlobal, noDecl, canThrow, stmtDepth) { var s = ''; if (rng(5) === 0) { // functions with functions. lower the recursion to prevent a mess. - s = 'function ' + name + '(' + createParams() + '){' + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth) + '}\n'; + s = [ + 'function ' + name + '(' + createParams() + '){', + strictMode(), + createFunctions(rng(5) + 1, Math.ceil(recurmax * 0.7), NOT_GLOBAL, ANY_TYPE, canThrow, stmtDepth), + '}', + '' + ].join('\n'); } else { // functions with statements - s = 'function ' + name + '(' + createParams() + '){' + createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}\n'; + s = [ + 'function ' + name + '(' + createParams() + '){', + strictMode(), + createStatements(3, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}', + '' + ].join('\n'); } VAR_NAMES.length = namesLenBefore; @@ -423,7 +450,7 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn } return '{var expr' + loop + ' = ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + '; ' + label.target + ' for (var key' + loop + ' in expr' + loop + ') {' + optElementVar + createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + '}}'; case STMT_SEMI: - return ';'; + return use_strict && rng(20) === 0 ? '"use strict";' : ';'; case STMT_EXPR: return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';'; case STMT_SWITCH: @@ -486,6 +513,7 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn // we have to do go through some trouble here to prevent leaking it var nameLenBefore = VAR_NAMES.length; var catchName = createVarName(MANDATORY); + if (catchName == 'this') catchName = 'a'; var freshCatchName = VAR_NAMES.length !== nameLenBefore; s += ' catch (' + catchName + ') { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + ' }'; if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1); // remove catch name @@ -563,37 +591,63 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { return createExpression(recurmax, COMMA_OK, stmtDepth, canThrow); case p++: return createExpression(recurmax, noComma, stmtDepth, canThrow) + '?' + createExpression(recurmax, NO_COMMA, stmtDepth, canThrow) + ':' + createExpression(recurmax, noComma, stmtDepth, canThrow); + case p++: case p++: var nameLenBefore = VAR_NAMES.length; var name = createVarName(MAYBE); // note: this name is only accessible from _within_ the function. and immutable at that. - if (name === 'c') name = 'a'; - var s = ''; - switch(rng(4)) { + if (name == 'c') name = 'a'; + var s = []; + switch (rng(5)) { case 0: - s = '(function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '})()'; + s.push( + '(function ' + name + '(){', + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '})()' + ); break; case 1: - s = '+function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + s.push( + '+function ' + name + '(){', + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}()' + ); break; case 2: - s = '!function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + s.push( + '!function ' + name + '(){', + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}()' + ); + break; + case 3: + s.push( + 'void function ' + name + '(){', + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}()' + ); break; default: - s = 'void function ' + name + '(){' + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth) + '}()'; + if (rng(4) == 0) s.push('function ' + name + '(){'); + else { + VAR_NAMES.push('this'); + s.push('new function ' + name + '(){'); + } + s.push( + strictMode(), + createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), + '}' + ); break; } VAR_NAMES.length = nameLenBefore; - return s; + return s.join('\n'); case p++: case p++: return createTypeofExpr(recurmax, stmtDepth, canThrow); - case p++: - return [ - 'new function() {', - rng(2) ? '' : createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', - 'return ' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ';', - '}' - ].join('\n'); case p++: case p++: // more like a parser test but perhaps comment nodes mess up the analysis? @@ -715,22 +769,24 @@ function _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { // intentionally generate more hardcore ops if (--recurmax < 0) return createValue(); + var assignee, expr; switch (rng(30)) { case 0: return '(c = c + 1, ' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; case 1: return '(' + createUnarySafePrefix() + '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + '))'; case 2: - var assignee = getVarName(); + assignee = getVarName(); + if (assignee == 'this') assignee = 'a'; return '(' + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; case 3: - var assignee = getVarName(); - var expr = '(' + assignee + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + assignee = getVarName(); + expr = '(' + assignee + '[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']' + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; case 4: - var assignee = getVarName(); - var expr = '(' + assignee + '.' + getDotKey() + createAssignment() + assignee = getVarName(); + expr = '(' + assignee + '.' + getDotKey() + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; return canThrow && rng(10) == 0 ? expr : '(' + assignee + ' && ' + expr + ')'; default: @@ -890,6 +946,12 @@ function log(options) { } else { console.log("// !!! uglify failed !!!"); console.log(uglify_code.stack); + if (typeof original_result != "string") { + console.log(); + console.log(); + console.log("original stacktrace:"); + console.log(original_result.stack); + } } console.log("minify(options):"); options = JSON.parse(options); @@ -901,6 +963,10 @@ function log(options) { } } +var fallback_options = [ JSON.stringify({ + compress: false, + mangle: false +}) ]; var minify_options = require("./ufuzz.json").map(JSON.stringify); var original_code, original_result; var uglify_code, uglify_result, ok; @@ -911,13 +977,9 @@ for (var round = 1; round <= num_iterations; round++) { loops = 0; funcs = 0; - original_code = [ - "var a = 100, b = 10, c = 0;", - createTopLevelCode(), - "console.log(null, a, b, c);" // preceding `null` makes for a cleaner output (empty string still shows up etc) - ].join("\n"); - - minify_options.forEach(function(options) { + original_code = createTopLevelCode(); + original_result = sandbox.run_code(original_code); + (typeof original_result != "string" ? fallback_options : minify_options).forEach(function(options) { try { uglify_code = UglifyJS.minify(original_code, JSON.parse(options)).code; } catch (e) { @@ -926,9 +988,10 @@ for (var round = 1; round <= num_iterations; round++) { ok = typeof uglify_code == "string"; if (ok) { - original_result = sandbox.run_code(original_code); uglify_result = sandbox.run_code(uglify_code); ok = sandbox.same_stdout(original_result, uglify_result); + } else if (typeof original_result != "string") { + ok = uglify_code.name == original_result.name; } if (verbose || (verbose_interval && !(round % INTERVAL_COUNT)) || !ok) log(options); else if (verbose_error && typeof original_result != "string") { From 9e626281716c0f11ed6b289d6a48c7b681a99a1e Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 24 Apr 2017 03:14:01 +0800 Subject: [PATCH 23/36] fix `unused` on for-in statements (#1843) Only need to avoid `var` within the initialisation block. fixes #1841 --- lib/compress.js | 2 +- test/compress/functions.js | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 5a8f23bf..0f4dd255 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1970,7 +1970,7 @@ merge(Compressor.prototype, { } return node; } - if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { + if (drop_vars && node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn && tt.parent().init === node)) { // place uninitialized names at the start var body = [], head = [], tail = []; // for unused names whose initialization has diff --git a/test/compress/functions.js b/test/compress/functions.js index dca40623..2a2d0965 100644 --- a/test/compress/functions.js +++ b/test/compress/functions.js @@ -93,3 +93,55 @@ issue_485_crashing_1530: { this, void 0; } } + +issue_1841_1: { + options = { + keep_fargs: false, + pure_getters: "strict", + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + !function(arg) { + for (var key in "hi") + var n = arg.baz, n = [ b = 42 ]; + }(--b); + console.log(b); + } + expect: { + var b = 10; + !function() { + for (var key in "hi") + b = 42; + }(--b); + console.log(b); + } + expect_exact: "42" +} + +issue_1841_2: { + options = { + keep_fargs: false, + pure_getters: false, + reduce_vars: true, + unused: true, + } + input: { + var b = 10; + !function(arg) { + for (var key in "hi") + var n = arg.baz, n = [ b = 42 ]; + }(--b); + console.log(b); + } + expect: { + var b = 10; + !function(arg) { + for (var key in "hi") + arg.baz, b = 42; + }(--b); + console.log(b); + } + expect_exact: "42" +} From 76d19b60ad5f6c7191eece8f8d224bdf4d920599 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Mon, 24 Apr 2017 03:15:03 +0800 Subject: [PATCH 24/36] fix fuzzer on `this` (#1842) - forbid redeclaration of `this` - suppress probability for `this` within nested functions --- test/ufuzz.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/ufuzz.js b/test/ufuzz.js index 12c62651..48e33f73 100644 --- a/test/ufuzz.js +++ b/test/ufuzz.js @@ -513,7 +513,6 @@ function createStatement(recurmax, canThrow, canBreak, canContinue, cannotReturn // we have to do go through some trouble here to prevent leaking it var nameLenBefore = VAR_NAMES.length; var catchName = createVarName(MANDATORY); - if (catchName == 'this') catchName = 'a'; var freshCatchName = VAR_NAMES.length !== nameLenBefore; s += ' catch (' + catchName + ') { ' + createStatements(3, recurmax, canThrow, canBreak, canContinue, cannotReturn, stmtDepth) + ' }'; if (freshCatchName) VAR_NAMES.splice(nameLenBefore, 1); // remove catch name @@ -631,13 +630,16 @@ function _createExpression(recurmax, noComma, stmtDepth, canThrow) { ); break; default: - if (rng(4) == 0) s.push('function ' + name + '(){'); - else { - VAR_NAMES.push('this'); - s.push('new function ' + name + '(){'); + var instantiate = rng(4) ? 'new ' : ''; + s.push( + instantiate + 'function ' + name + '(){', + strictMode() + ); + if (instantiate) for (var i = rng(4); --i >= 0;) { + if (rng(2)) s.push('this.' + getDotKey() + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ';'); + else s.push('this[' + createExpression(recurmax, COMMA_OK, stmtDepth, canThrow) + ']' + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ';'); } s.push( - strictMode(), createStatements(rng(5) + 1, recurmax, canThrow, CANNOT_BREAK, CANNOT_CONTINUE, CAN_RETURN, stmtDepth), '}' ); @@ -777,7 +779,6 @@ function _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) { return '(' + createUnarySafePrefix() + '(' + _createSimpleBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + '))'; case 2: assignee = getVarName(); - if (assignee == 'this') assignee = 'a'; return '(' + assignee + createAssignment() + _createBinaryExpr(recurmax, noComma, stmtDepth, canThrow) + ')'; case 3: assignee = getVarName(); From bbb5f2a89c9b68b35aec96ccc48a9d0ef250780a Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 25 Apr 2017 13:30:43 -0400 Subject: [PATCH 25/36] Update ISSUE_TEMPLATE.md (#1846) --- .github/ISSUE_TEMPLATE.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 44afb511..577bb665 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,8 +1,19 @@ -- Bug report or feature request? -- `uglify-js` version (`uglifyjs -V`) -- JavaScript input - ideally as small as possible. -- The `uglifyjs` CLI command executed or `minify()` options used. -- An example of JavaScript output produced and/or the error or warning. +**Bug report or feature request?** + + + +**ES5 or ES6+ input?** + + + +**`uglify-js` version (`uglifyjs -V`)** + +**JavaScript input - ideally as small as possible.** + +**The `uglifyjs` CLI command executed or `minify()` options used.** + +**JavaScript output produced and/or the error or warning.** +