diff --git a/README.md b/README.md index 67324dbd..9118d663 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ The available options are: --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. + 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 @@ -190,11 +192,6 @@ input files from the command line. To enable the mangler you need to pass `--mangle` (`-m`). The following (comma-separated) options are supported: -- `sort` — to assign shorter names to most frequently used variables. This - saves a few hundred bytes on jQuery before gzip, but the output is - _bigger_ after gzip (and seems to happen for other libraries I tried it - on) therefore it's not enabled by default. - - `toplevel` — mangle names declared in the toplevel scope (disabled by default). @@ -323,6 +320,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` +- `collapse_vars` -- default `false`. Collapse single-use `var` and `const` + definitions when possible. + - `warnings` -- display warnings when dropping unreachable code or unused declarations etc. @@ -395,6 +395,8 @@ separate file and include it into the build. For example you can have a ```javascript const DEBUG = false; const PRODUCTION = true; +// Alternative for environments that don't support `const` +/** @const */ var STAGING = false; // etc. ``` @@ -404,10 +406,26 @@ and build your code like this: UglifyJS will notice the constants and, since they cannot be altered, it will evaluate references to them to the value itself and drop unreachable -code as usual. The possible downside of this approach is that the build -will contain the `const` declarations. +code as usual. The build will contain the `const` declarations if you use +them. If you are targeting < ES6 environments, use `/** @const */ var`. + +#### 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: + +```js +uglifyJS.minify([ "input.js"], { + compress: { + dead_code: true, + global_defs: { + DEBUG: false + } + } +}); +``` + ## Beautifier options The code generator tries to output shortest code possible by default. In @@ -624,6 +642,9 @@ Other options: - `mangle` — pass `false` to skip mangling names. +- `mangleProperties` (default `false`) — pass an object to specify custom + mangle property options. + - `output` (default `null`) — pass an object if you wish to specify additional [output options][codegen]. The defaults are optimized for best compression. @@ -631,6 +652,13 @@ Other options: - `compress` (default `{}`) — pass `false` to skip compressing entirely. Pass an object to specify custom [compressor options][compressor]. +- `parse` (default {}) — pass an object if you wish to specify some + additional [parser options][parser]. (not all options available... see below) + +##### mangleProperties options + + - `regex` — Pass a RegExp to only mangle certain names (maps to the `--mange-regex` CLI arguments option) + We could add more options to `UglifyJS.minify` — if you need additional functionality please suggest! @@ -649,6 +677,9 @@ 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`) @@ -788,3 +819,4 @@ The `source_map_options` (optional) can contain the following properties: [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 diff --git a/bin/uglifyjs b/bin/uglifyjs index f7f22215..90197cc4 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -499,17 +499,19 @@ function normalize(o) { } } -function getOptions(x, constants) { - x = ARGS[x]; - if (x == null) return null; +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, { expression: true }); } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { - print_error("Error parsing arguments in: " + x); + print_error("Error parsing arguments for flag `" + flag + "': " + x); process.exit(1); } } @@ -529,7 +531,7 @@ function getOptions(x, constants) { return true; // no descend } print_error(node.TYPE) - print_error("Error parsing arguments in: " + x); + print_error("Error parsing arguments for flag `" + flag + "': " + x); process.exit(1); })); } diff --git a/lib/ast.js b/lib/ast.js index e6e39d75..a0d2aac1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -71,7 +71,7 @@ function DEFNODE(type, props, methods, base) { if (type) { ctor.prototype.TYPE = ctor.TYPE = type; } - if (methods) for (i in methods) if (methods.hasOwnProperty(i)) { + if (methods) for (i in methods) if (HOP(methods, i)) { if (/^\$/.test(i)) { ctor[i.substr(1)] = methods[i]; } else { @@ -294,7 +294,7 @@ var AST_Scope = DEFNODE("Scope", "is_block_scope directives variables functions parent_scope: "[AST_Scope?/S] link to the parent scope", enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes", cname: "[integer/S] current index for mangling variables (used internally by the mangler)", - }, + } }, AST_Block); var AST_Toplevel = DEFNODE("Toplevel", "globals", { @@ -849,6 +849,13 @@ var AST_Seq = DEFNODE("Seq", "car cdr", { p = p.cdr; } }, + len: function() { + if (this.cdr instanceof AST_Seq) { + return this.cdr.len() + 1; + } else { + return 2; + } + }, _walk: function(visitor) { return visitor._visit(this, function(){ this.car._walk(visitor); diff --git a/lib/compress.js b/lib/compress.js index 9c2dcbba..699a95b6 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -66,6 +66,7 @@ function Compressor(options, false_by_default) { hoist_vars : false, if_return : !false_by_default, join_vars : !false_by_default, + collapse_vars : false, cascade : !false_by_default, side_effects : !false_by_default, pure_getters : false, @@ -175,6 +176,23 @@ merge(Compressor.prototype, { } }; + // we shouldn't compress (1,func)(something) to + // func(something) because that changes the meaning of + // the func (becomes lexical instead of global). + function maintain_this_binding(parent, orig, val) { + if (parent instanceof AST_Call && parent.expression === orig) { + if (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 val; + } + function as_statement_array(thing) { if (thing === null) return []; if (thing instanceof AST_BlockStatement) return thing.body; @@ -226,6 +244,9 @@ merge(Compressor.prototype, { if (compressor.option("join_vars")) { statements = join_consecutive_vars(statements, compressor); } + if (compressor.option("collapse_vars")) { + statements = collapse_single_use_vars(statements, compressor); + } } while (CHANGED && max_iter-- > 0); if (compressor.option("negate_iife")) { @@ -234,6 +255,163 @@ merge(Compressor.prototype, { return statements; + function collapse_single_use_vars(statements, compressor) { + // Iterate statements backwards looking for a statement with a var/const + // declaration immediately preceding it. Grab the rightmost var definition + // 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; + 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 side_effects_encountered = false; + var lvalues_encountered = false; + var lvalues = {}; + + // 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") { + side_effects_encountered = true; + continue; + } + 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(compressor)) { + var ctt = new TreeTransformer(function(node) { + if (node === ref) + return replace_var(node, ctt.parent(), true); + }); + stat.transform(ctt); + continue; + } + + // Restrict var replacement to constants if side effects encountered. + if (side_effects_encountered |= lvalues_encountered) continue; + + // Non-constant single use vars can only be replaced in same scope. + if (ref.scope !== self) { + side_effects_encountered |= var_decl.value.has_side_effects(compressor); + 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) + || (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 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 is_lvalue(node, parent) { + return node instanceof AST_SymbolRef && ( + (parent instanceof AST_Assign && node === parent.left) + || (parent instanceof AST_Unary && parent.expression === node + && (parent.operator == "++" || parent.operator == "--"))); + } + function replace_var(node, parent, is_constant) { + if (is_lvalue(node, parent)) return node; + + // Remove var definition and return its value to the TreeTransformer to replace. + var value = maintain_this_binding(parent, node, var_decl.value); + var_decl.value = null; + + 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; + } + // Further optimize statement after substitution. + stat.walk(new TreeWalker(function(node){ + delete node._squeezed; + delete node._optimized; + })); + + compressor.warn("Replacing " + (is_constant ? "constant" : "variable") + + " " + var_name + " [{file}:{line},{col}]", node.start); + CHANGED = true; + return value; + } + } + function process_for_angular(statements) { function has_inject(comment) { return /@ngInject/.test(comment.value); @@ -511,8 +689,12 @@ merge(Compressor.prototype, { seq = []; }; statements.forEach(function(stat){ - if (stat instanceof AST_SimpleStatement && seq.length < 2000) seq.push(stat.body); - else push_seq(), ret.push(stat); + if (stat instanceof AST_SimpleStatement && seqLength(seq) < 2000) { + seq.push(stat.body); + } else { + push_seq(); + ret.push(stat); + } }); push_seq(); ret = sequencesize_2(ret, compressor); @@ -520,6 +702,18 @@ 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(); @@ -639,7 +833,9 @@ merge(Compressor.prototype, { }; function extract_declarations_from_unreachable_code(compressor, stat, target) { - compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); + if (!(stat instanceof AST_Defun)) { + compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); + } stat.walk(new TreeWalker(function(node){ if (node instanceof AST_Var) { compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start); @@ -854,8 +1050,16 @@ merge(Compressor.prototype, { : ev(this.alternative, compressor); }); def(AST_SymbolRef, function(compressor){ - var d = this.definition(); - if (d && d.constant && d.init) return ev(d.init, compressor); + if (this._evaluating) throw def; + this._evaluating = true; + try { + var d = this.definition(); + if (d && d.constant && d.init) { + return ev(d.init, compressor); + } + } finally { + this._evaluating = false; + } throw def; }); def(AST_Dot, function(compressor){ @@ -1092,13 +1296,14 @@ merge(Compressor.prototype, { && !self.uses_eval ) { var in_use = []; + var in_use_ids = {}; // avoid expensive linear scans of in_use var initializations = new Dictionary(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). var scope = this; var tw = new TreeWalker(function(node, descend){ if (node !== self) { - if (node instanceof AST_Defun) { + if (node instanceof AST_Defun || node instanceof AST_DefClass) { initializations.add(node.name.name, node); return true; // don't go in nested scopes } @@ -1115,7 +1320,11 @@ merge(Compressor.prototype, { return true; } if (node instanceof AST_SymbolRef) { - push_uniq(in_use, node.definition()); + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); + } return true; } if (node instanceof AST_Scope) { @@ -1138,7 +1347,11 @@ merge(Compressor.prototype, { if (init) init.forEach(function(init){ var tw = new TreeWalker(function(node){ if (node instanceof AST_SymbolRef) { - push_uniq(in_use, node.definition()); + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); + } } }); init.walk(tw); @@ -1178,9 +1391,7 @@ merge(Compressor.prototype, { } } if ((node instanceof AST_Defun || node instanceof AST_DefClass) && node !== self) { - var keep = - member(node.name.definition(), in_use) || - node.name.definition().global; + var keep = (node.name.definition().id in in_use_ids) || node.name.definition().global; if (!keep) { compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, @@ -1195,8 +1406,9 @@ merge(Compressor.prototype, { if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ if (def.is_destructuring()) return true; - if (member(def.name.definition(), in_use)) return true; + if (def.name.definition().id in in_use_ids) return true; if (def.name.definition().global) return true; + var w = { name : def.name.name, file : def.name.start.file, @@ -1333,7 +1545,10 @@ merge(Compressor.prototype, { var seq = node.to_assignments(); var p = tt.parent(); if (p instanceof AST_ForIn && p.init === node) { - if (seq == null) return node.definitions[0].name; + if (seq == null) { + var def = node.definitions[0].name; + return make_node(AST_SymbolRef, def, def); + } return seq; } if (p instanceof AST_For && p.init === node) { @@ -1564,9 +1779,13 @@ merge(Compressor.prototype, { } if (is_empty(self.alternative)) self.alternative = null; var negated = self.condition.negate(compressor); - var negated_is_best = best_of(self.condition, negated) === negated; + var self_condition_length = self.condition.print_to_string().length; + var negated_length = negated.print_to_string().length; + var negated_is_best = negated_length < self_condition_length; if (self.alternative && negated_is_best) { negated_is_best = false; // because we already do the switch here. + // no need to swap values of self_condition_length and negated_length + // here because they are only used in an equality comparison later on. self.condition = negated; var tmp = self.body; self.body = self.alternative || make_node(AST_EmptyStatement); @@ -1588,6 +1807,13 @@ merge(Compressor.prototype, { }).transform(compressor); } if (is_empty(self.alternative) && self.body instanceof AST_SimpleStatement) { + if (self_condition_length === negated_length && !negated_is_best + && self.condition instanceof AST_Binary && self.condition.operator == "||") { + // although the code length of self.condition and negated are the same, + // negated does not require additional surrounding parentheses. + // see https://github.com/mishoo/UglifyJS2/issues/979 + negated_is_best = true; + } if (negated_is_best) return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "||", @@ -2020,13 +2246,7 @@ merge(Compressor.prototype, { if (!compressor.option("side_effects")) return self; if (!self.car.has_side_effects(compressor)) { - // we shouldn't compress (1,func)(something) to - // func(something) because that changes the meaning of - // the func (becomes lexical instead of global). - var p = compressor.parent(); - if (!(p instanceof AST_Call && p.expression === self)) { - return self.cdr; - } + return maintain_this_binding(compressor.parent(), self, self.cdr); } if (compressor.option("cascade")) { if (self.car instanceof AST_Assign @@ -2216,11 +2436,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); - var rr = self.right.evaluate(compressor); - return rr[0]; + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } else { compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return ll[0]; + return maintain_this_binding(compressor.parent(), self, ll[0]); } } } @@ -2229,11 +2448,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); - return ll[0]; + return maintain_this_binding(compressor.parent(), self, ll[0]); } else { compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); - var rr = self.right.evaluate(compressor); - return rr[0]; + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } } } @@ -2392,7 +2610,7 @@ merge(Compressor.prototype, { if (self.undeclared() && !isLHS(self, compressor.parent())) { var defines = compressor.option("global_defs"); - if (defines && defines.hasOwnProperty(self.name)) { + if (defines && HOP(defines, self.name)) { return make_node_from_constant(compressor, defines[self.name], self); } switch (self.name) { @@ -2458,10 +2676,10 @@ merge(Compressor.prototype, { if (cond.length > 1) { if (cond[1]) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); - return self.consequent; + return maintain_this_binding(compressor.parent(), self, self.consequent); } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.start); - return self.alternative; + return maintain_this_binding(compressor.parent(), self, self.alternative); } } var negated = cond[0].negate(compressor); @@ -2541,20 +2759,58 @@ merge(Compressor.prototype, { } } - // y?true:false --> !!y - if (is_true(consequent) && is_false(alternative)) { - self.condition = self.condition.negate(compressor); - return make_node(AST_UnaryPrefix, self.condition, { - operator: "!", - expression: self.condition + if (is_true(self.consequent)) { + if (is_false(self.alternative)) { + // c ? true : false ---> !!c + return booleanize(self.condition); + } + // c ? true : x ---> !!c || x + return make_node(AST_Binary, self, { + operator: "||", + left: booleanize(self.condition), + right: self.alternative }); } - // y?false:true --> !y - if (is_false(consequent) && is_true(alternative)) { - return self.condition.negate(compressor) + if (is_false(self.consequent)) { + if (is_true(self.alternative)) { + // c ? false : true ---> !c + return booleanize(self.condition.negate(compressor)); + } + // c ? false : x ---> !c && x + return make_node(AST_Binary, self, { + operator: "&&", + left: booleanize(self.condition.negate(compressor)), + right: self.alternative + }); } + if (is_true(self.alternative)) { + // c ? x : true ---> !c || x + return make_node(AST_Binary, self, { + operator: "||", + left: booleanize(self.condition.negate(compressor)), + right: self.consequent + }); + } + if (is_false(self.alternative)) { + // c ? x : false ---> !!c && x + return make_node(AST_Binary, self, { + operator: "&&", + left: booleanize(self.condition), + right: self.consequent + }); + } + return self; + function booleanize(node) { + if (node.is_boolean()) return node; + // !!expression + return make_node(AST_UnaryPrefix, node, { + operator: "!", + expression: node.negate(compressor) + }); + } + // AST_True or !0 function is_true(node) { return node instanceof AST_True diff --git a/lib/output.js b/lib/output.js index 827ba98e..002983e2 100644 --- a/lib/output.js +++ b/lib/output.js @@ -74,7 +74,7 @@ function OutputStream(options) { var OUTPUT = ""; function to_ascii(str, identifier) { - return str.replace(/[\u0080-\uffff]/g, function(ch) { + return str.replace(/[\u0000-\u001f\u007f-\uffff]/g, function(ch) { var code = ch.charCodeAt(0).toString(16); if (code.length <= 2 && !identifier) { while (code.length < 2) code = "0" + code; @@ -90,16 +90,17 @@ function OutputStream(options) { var dq = 0, sq = 0; str = str.replace(/[\\\b\f\n\r\v\t\x22\x27\u2028\u2029\0\ufeff]/g, function(s){ switch (s) { + case '"': ++dq; return '"'; + case "'": ++sq; return "'"; case "\\": return "\\\\"; - case "\b": return "\\b"; - case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; + case "\t": return "\\t"; + case "\b": return "\\b"; + case "\f": return "\\f"; case "\x0B": return options.screw_ie8 ? "\\v" : "\\x0B"; case "\u2028": return "\\u2028"; case "\u2029": return "\\u2029"; - case '"': ++dq; return '"'; - case "'": ++sq; return "'"; case "\0": return "\\x00"; case "\ufeff": return "\\ufeff"; } @@ -449,11 +450,11 @@ function OutputStream(options) { }); } else if (c.test) { comments = comments.filter(function(comment){ - return c.test(comment.value) || comment.type == "comment5"; + return comment.type == "comment5" || c.test(comment.value); }); } else if (typeof c == "function") { comments = comments.filter(function(comment){ - return c(self, comment) || comment.type == "comment5"; + return comment.type == "comment5" || c(self, comment); }); } @@ -601,8 +602,12 @@ function OutputStream(options) { PARENS(AST_Number, function(output){ var p = output.parent(); - if (this.getValue() < 0 && p instanceof AST_PropAccess && p.expression === this) - return true; + if (p instanceof AST_PropAccess && p.expression === this) { + var value = this.getValue(); + if (value < 0 || /^0/.test(make_num(value))) { + return true; + } + } }); PARENS([ AST_Assign, AST_Conditional ], function (output){ @@ -1180,7 +1185,7 @@ function OutputStream(options) { var expr = self.expression; expr.print(output); if (expr instanceof AST_Number && expr.getValue() >= 0) { - if (!/[xa-f.]/i.test(output.last())) { + if (!/[xa-f.)]/i.test(output.last())) { output.print("."); } } diff --git a/lib/parse.js b/lib/parse.js index b9eeb63e..aa0f32d5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -46,7 +46,7 @@ var KEYWORDS = 'break case catch class const continue debugger default delete do else export extends finally for function if in instanceof new return switch throw try typeof var let void while with import yield'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char class double enum export extends final float goto implements import int interface let long native package private protected public short static super synchronized this throws transient volatile' + " " + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; @@ -409,7 +409,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { if (octal_len > 0) ch = String.fromCharCode(parseInt(ch, 8)); else ch = read_escaped_char(true); } - else if (ch == "\n") parse_error("Unterminated string constant"); + else if ("\r\n\u2028\u2029".indexOf(ch) >= 0) parse_error("Unterminated string constant"); else if (ch == quote) break; ret += ch; } @@ -431,7 +431,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { S.col = S.tokcol + (S.pos - S.tokpos); S.comments_before.push(token(type, ret, true)); S.regex_allowed = regex_allowed; - return next_token(); + return next_token; }; var skip_multiline_comment = with_eof_error("Unterminated multiline comment", function(){ @@ -449,7 +449,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { S.comments_before.push(token("comment2", text, true)); S.regex_allowed = regex_allowed; S.newline_before = nlb; - return next_token(); + return next_token; }); function read_name() { @@ -575,37 +575,46 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { function next_token(force_regexp) { if (force_regexp != null) return read_regexp(force_regexp); - skip_whitespace(); - start_token(); - if (html5_comments) { - if (looking_at("") && S.newline_before) { + forward(3); + skip_line_comment("comment4"); + continue; + } } - if (looking_at("-->") && S.newline_before) { - forward(3); - return skip_line_comment("comment4"); + var ch = peek(); + if (!ch) return token("eof"); + var code = ch.charCodeAt(0); + switch (code) { + case 34: case 39: return read_string(ch); + case 46: return handle_dot(); + case 47: { + var tok = handle_slash(); + if (tok === next_token) continue; + return tok; + } + case 61: return handle_eq_sign(); } - } - var ch = peek(); - if (!ch) return token("eof"); - var code = ch.charCodeAt(0); - switch (code) { - case 34: case 39: return read_string(ch); - case 46: return handle_dot(); - case 47: return handle_slash(); - case 61: return handle_eq_sign(); - } - if (is_digit(code)) return read_num(); - if (PUNC_CHARS(ch)) return token("punc", next()); - if (OPERATOR_CHARS(ch)) return read_operator(); - if (code == 92 || is_identifier_start(code)) return read_word(); - - if (shebang) { - if (S.pos == 0 && looking_at("#!")) { - forward(2); - return skip_line_comment("comment5"); + if (is_digit(code)) return read_num(); + if (PUNC_CHARS(ch)) return token("punc", next()); + if (OPERATOR_CHARS(ch)) return read_operator(); + if (code == 92 || is_identifier_start(code)) return read_word(); + if (shebang) { + if (S.pos == 0 && looking_at("#!")) { + forward(2); + skip_line_comment("comment5"); + continue; + } } + break; } parse_error("Unexpected character '" + ch + "'"); }; @@ -1379,6 +1388,13 @@ function parse($TEXT, options) { break; } break; + case "operator": + if (!is_identifier_string(tok.value)) { + throw new JS_Parse_Error("Invalid getter/setter name: " + tok.value, + tok.file, tok.line, tok.col, tok.pos); + } + ret = _make_symbol(AST_SymbolRef); + break; } next(); return ret; @@ -1509,7 +1525,7 @@ function parse($TEXT, options) { })); continue; } - + if (is("operator", "=")) { next(); a.push(new AST_Assign({ diff --git a/lib/scope.js b/lib/scope.js index ef92329b..39bd9cbf 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -55,8 +55,11 @@ function SymbolDef(scope, index, orig) { this.undeclared = false; this.constant = false; this.index = index; + this.id = SymbolDef.next_id++; }; +SymbolDef.next_id = 1; + SymbolDef.prototype = { unmangleable: function(options) { if (!options) options = {}; @@ -102,6 +105,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var labels = new Dictionary(); var defun = null; + var last_var_had_const_pragma = false; var nesting = 0; var in_destructuring = null; var in_export; @@ -195,11 +199,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // inside the class. (node.scope = defun.parent_scope).def_function(node, in_export); } + else if (node instanceof AST_Var) { + last_var_had_const_pragma = node.has_const_pragma(); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst || node instanceof AST_SymbolLet) { var def = ((node instanceof AST_SymbolBlockDeclaration) ? scope : defun).def_variable(node, in_export); - def.constant = node instanceof AST_SymbolConst; + def.constant = node instanceof AST_SymbolConst || last_var_had_const_pragma; def.destructuring = in_destructuring; def.init = tw.parent().value; } @@ -244,6 +251,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolRef) { var name = node.name; + if (name == "eval" && tw.parent() instanceof AST_Call) { + for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) { + s.uses_eval = true; + } + } var sym = node.scope.find_variable(name); if (!sym) { var g; @@ -256,10 +268,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ globals.set(name, g); } node.thedef = g; - if (name == "eval" && tw.parent() instanceof AST_Call) { - for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) - s.uses_eval = true; - } if (func && name == "arguments") { func.uses_arguments = true; } @@ -299,6 +307,10 @@ AST_Block.DEFMETHOD("creates_block_scope", function() { AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; + + var symbol = new AST_VarDef({ name: "arguments", start: this.start, end: this.end }); + var def = new SymbolDef(this, this.variables.size(), symbol); + this.variables.set(symbol.name, def); }); AST_SymbolRef.DEFMETHOD("reference", function() { @@ -420,11 +432,17 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); +AST_Var.DEFMETHOD("has_const_pragma", function() { + var comments_before = this.start && this.start.comments_before; + var lastComment = comments_before && comments_before[comments_before.length - 1]; + return lastComment && /@const\b/.test(lastComment.value); +}); + AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { except : [], eval : false, - sort : false, + sort : false, // Ignored. Flag retained for backwards compatibility. toplevel : false, screw_ie8 : false, keep_fnames : false, @@ -434,6 +452,10 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ options = this._default_mangler_options(options); + + // Never mangle arguments + options.except.push('arguments'); + // We only need to mangle declaration nodes. Special logic wired // into the code generator will display the mangled name if it's // present (and for AST_SymbolRef-s it'll use the mangled name of @@ -464,9 +486,6 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ a.push(symbol); } }); - if (options.sort) a.sort(function(a, b){ - return b.references.length - a.references.length; - }); to_mangle.push.apply(to_mangle, a); return; } diff --git a/lib/sourcemap.js b/lib/sourcemap.js index a67011f0..e5d7df60 100644 --- a/lib/sourcemap.js +++ b/lib/sourcemap.js @@ -53,16 +53,11 @@ function SourceMap(options) { orig_line_diff : 0, dest_line_diff : 0, }); + var generator = new MOZ_SourceMap.SourceMapGenerator({ + file : options.file, + sourceRoot : options.root + }); var orig_map = options.orig && new MOZ_SourceMap.SourceMapConsumer(options.orig); - var generator; - if (orig_map) { - generator = MOZ_SourceMap.SourceMapGenerator.fromSourceMap(orig_map); - } else { - generator = new MOZ_SourceMap.SourceMapGenerator({ - file : options.file, - sourceRoot : options.root - }); - } function add(source, gen_line, gen_col, orig_line, orig_col, name) { if (orig_map) { var info = orig_map.originalPositionFor({ @@ -83,7 +78,7 @@ function SourceMap(options) { source : source, name : name }); - } + }; return { add : add, get : function() { return generator }, diff --git a/lib/transform.js b/lib/transform.js index dc3a068f..34663351 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -64,7 +64,7 @@ TreeTransformer.prototype = new TreeWalker; x = this; descend(x, tw); } else { - tw.stack[tw.stack.length - 1] = x = this.clone(); + tw.stack[tw.stack.length - 1] = x = this; descend(x, tw); y = tw.after(x, in_list); if (y !== undefined) x = y; diff --git a/lib/utils.js b/lib/utils.js index 4612a322..78c6dbf7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -59,10 +59,7 @@ function characters(str) { }; function member(name, array) { - for (var i = array.length; --i >= 0;) - if (array[i] == name) - return true; - return false; + return array.indexOf(name) >= 0; }; function find_if(func, array) { @@ -97,17 +94,17 @@ function defaults(args, defs, croak) { if (args === true) args = {}; var ret = args || {}; - if (croak) for (var i in ret) if (ret.hasOwnProperty(i) && !defs.hasOwnProperty(i)) + if (croak) for (var i in ret) if (HOP(ret, i) && !HOP(defs, i)) DefaultsError.croak("`" + i + "` is not a supported option", defs); - for (var i in defs) if (defs.hasOwnProperty(i)) { - ret[i] = (args && args.hasOwnProperty(i)) ? args[i] : defs[i]; + for (var i in defs) if (HOP(defs, i)) { + ret[i] = (args && HOP(args, i)) ? args[i] : defs[i]; } return ret; }; function merge(obj, ext) { var count = 0; - for (var i in ext) if (ext.hasOwnProperty(i)) { + for (var i in ext) if (HOP(ext, i)) { obj[i] = ext[i]; count++; } @@ -150,7 +147,7 @@ var MAP = (function(){ } } else { - for (i in a) if (a.hasOwnProperty(i)) if (doit()) break; + for (i in a) if (HOP(a, i)) if (doit()) break; } return top.concat(ret); }; @@ -308,3 +305,7 @@ Dictionary.fromObject = function(obj) { dict._size = merge(dict._values, obj); return dict; }; + +function HOP(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} diff --git a/package.json b/package.json index 6b0d2f40..748e04fb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.6.1", + "version": "2.6.2", "engines": { "node": ">=0.8.0" }, @@ -38,7 +38,8 @@ "acorn": "~0.6.0", "escodegen": "~1.3.3", "esfuzz": "~0.3.1", - "estraverse": "~1.5.1" + "estraverse": "~1.5.1", + "mocha": "~2.3.4" }, "browserify": { "transform": [ @@ -48,5 +49,6 @@ "scripts": { "shrinkwrap": "rm ./npm-shrinkwrap.json; rm -rf ./node_modules; npm i && npm shrinkwrap && npm outdated", "test": "node test/run-tests.js" - } + }, + "keywords": ["uglify", "uglify-js", "minify", "minifier"] } diff --git a/test/compress/ascii.js b/test/compress/ascii.js new file mode 100644 index 00000000..5c6b3b8e --- /dev/null +++ b/test/compress/ascii.js @@ -0,0 +1,32 @@ +ascii_only_true: { + options = {} + beautify = { + ascii_only : true, + beautify : false, + } + input: { + function f() { + return "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + + "\x20\x21\x22\x23 ... \x7d\x7e\x7f\x80\x81 ... \xfe\xff\u0fff\uffff"; + } + } + expect_exact: 'function f(){return"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\b\\t\\n\\x0B\\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\'}' +} + +ascii_only_false: { + options = {} + beautify = { + ascii_only : false, + beautify : false, + } + input: { + function f() { + return "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + + "\x20\x21\x22\x23 ... \x7d\x7e\x7f\x80\x81 ... \xfe\xff\u0fff\uffff"; + } + } + expect_exact: 'function f(){return"\\x00\x01\x02\x03\x04\x05\x06\x07\\b\\t\\n\\x0B\\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/block-scope.js b/test/compress/block-scope.js index 47a38f74..4c8ca796 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -130,4 +130,3 @@ regression_block_scope_resolves: { }()); } } - diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js new file mode 100644 index 00000000..934a5c73 --- /dev/null +++ b/test/compress/collapse_vars.js @@ -0,0 +1,1155 @@ +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 + } + input: { + function f1() { + var e = 7; + var s = "abcdef"; + var i = 2; + var log = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, y, z, e); + } + function f2() { + var e = 7; + var log = console.log.bind(console); + var s = "abcdef"; + var i = 2; + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, i, y, z, e); + } + function f3() { + var e = 7; + var s = "abcdef"; + var i = 2; + var log = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, z, y, e); + } + function f4() { + var log = console.log.bind(console), + i = 10, + x = i += 2, + y = i += 3, + z = i += 4; + log(x, z, y, i); + } + } + expect: { + function f1() { + var s = "abcdef", i = 2; + console.log.bind(console)(s.charAt(i++), s.charAt(i++), s.charAt(i++), 7); + } + function f2() { + var log = console.log.bind(console), + s = "abcdef", + i = 2, + x = s.charAt(i++), + y = s.charAt(i++), + z = s.charAt(i++); + log(x, i, y, z, 7); + } + function f3() { + var s = "abcdef", + i = 2, + log = console.log.bind(console), + x = s.charAt(i++), + y = s.charAt(i++); + log(x, s.charAt(i++), y, 7); + } + function f4() { + var log = console.log.bind(console), + i = 10, + x = i += 2, + y = i += 3; + log(x, i += 4, y, i); + } + } +} + +collapse_vars_side_effects_2: { + 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: { + function fn(x) { return console.log(x), x; } + + function p1() { var a = foo(), b = bar(), c = baz(); return a + b + c; } + function p2() { var a = foo(), c = bar(), b = baz(); return a + b + c; } + function p3() { var b = foo(), a = bar(), c = baz(); return a + b + c; } + function p4() { var b = foo(), c = bar(), a = baz(); return a + b + c; } + function p5() { var c = foo(), a = bar(), b = baz(); return a + b + c; } + function p6() { var c = foo(), b = bar(), a = baz(); return a + b + c; } + + function q1() { var a = foo(), b = bar(), c = baz(); return fn(a + b + c); } + function q2() { var a = foo(), c = bar(), b = baz(); return fn(a + b + c); } + function q3() { var b = foo(), a = bar(), c = baz(); return fn(a + b + c); } + function q4() { var b = foo(), c = bar(), a = baz(); return fn(a + b + c); } + function q5() { var c = foo(), a = bar(), b = baz(); return fn(a + b + c); } + function q6() { var c = foo(), b = bar(), a = baz(); return fn(a + b + c); } + + function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r4() { var b = foo(), c = bar(), a = baz(); return fn(a) + fn(b) + fn(c); } + function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r6() { var c = foo(), b = bar(), a = baz(); return fn(a) + fn(b) + fn(c); } + + function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); } + function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); } + + function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); } + function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); } + } + expect: { + function fn(x) { return console.log(x), x; } + + function p1() { return foo() + bar() + baz(); } + function p2() { var a = foo(), c = bar(); return a + baz() + c; } + function p3() { var b = foo(); return bar() + b + baz(); } + function p4() { var b = foo(), c = bar(); return baz() + b + c; } + function p5() { var c = foo(); return bar() + baz() + c; } + function p6() { var c = foo(), b = bar(); return baz() + b + c; } + + function q1() { return fn(foo() + bar() + baz()); } + function q2() { var a = foo(), c = bar(); return fn(a + baz() + c); } + function q3() { var b = foo(); return fn(bar() + b + baz()); } + function q4() { var b = foo(), c = bar(); return fn(baz() + b + c); } + function q5() { var c = foo(); return fn(bar() + baz() + c); } + function q6() { var c = foo(), b = bar(); return fn(baz() + b + c); } + + function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r4() { var b = foo(), c = bar(); return fn(baz()) + fn(b) + fn(c); } + function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r6() { var c = foo(), b = bar(); return fn(baz()) + fn(b) + fn(c); } + + function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); } + function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); } + + function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); } + function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); } + } +} + +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 + } + input: { + define(["require", "exports", 'handlebars'], function (require, exports, hb) { + var win = window; + var _hb = win.Handlebars = hb; + return _hb; + }); + def(function (hb) { + var win = window; + var prop = 'Handlebars'; + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = 'Handlebars'; + var win = window; + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = 'Handlebars'; + var win = g(); + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = g1(); + var win = g2(); + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var win = g2(); + var prop = g1(); + var _hb = win[prop] = hb; + return _hb; + }); + } + expect: { + define([ "require", "exports", "handlebars" ], function(require, exports, hb) { + return window.Handlebars = hb; + }), + def(function(hb) { + return window.Handlebars = hb; + }), + def(function(hb) { + return window.Handlebars = hb; + }), + def(function (hb) { + return g().Handlebars = hb; + }), + def(function (hb) { + var prop = g1(); + return g2()[prop] = hb; + }), + def(function (hb) { + return g2()[g1()] = hb; + }); + } +} + +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 + } + input: { + function f1(obj) { + var prop = 'LiteralProperty'; + return !!-+obj[prop]; + } + function f2(obj) { + var prop1 = 'One'; + var prop2 = 'Two'; + return ~!!-+obj[prop1 + prop2]; + } + } + expect: { + function f1(obj) { + return !!-+obj.LiteralProperty; + } + function f2(obj) { + return ~!!-+obj.OneTwo; + } + } +} + +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 + } + input: { + function f1() { + var not_used = sideeffect(), x = g1 + g2; + var y = x / 4, z = 'Bar' + y; + if ('x' != z) { return g9; } + else return g5; + } + function f2() { + var x = g1 + g2, not_used = sideeffect(); + var y = x / 4 + var z = 'Bar' + y; + if ('x' != z) { return g9; } + else return g5; + } + function f3(x) { + if (x) { + var a = 1; + return a; + } + else { + var b = 2; + return b; + } + } + } + expect: { + function f1() { + sideeffect(); + return "x" != "Bar" + (g1 + g2) / 4 ? g9 : g5; + } + function f2() { + var x = g1 + g2; + sideeffect(); + return "x" != "Bar" + x / 4 ? g9 : g5; + } + function f3(x) { + if (x) { + return 1; + } + return 2; + } + } +} + +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 + } + input: { + function f1(y) { + // Neither the non-constant while condition `c` will be + // replaced, nor the non-constant `x` in the body. + var x = y, c = 3 - y; + while (c) { return x; } + var z = y * y; + return z; + } + function f2(y) { + // The constant `x` will be replaced in the while body. + var x = 7; + while (y) { return x; } + var z = y * y; + return z; + } + function f3(y) { + // The non-constant `n` will not be replaced in the while body. + var n = 5 - y; + while (y) { return n; } + var z = y * y; + return z; + } + } + expect: { + function f1(y) { + var x = y, c = 3 - y; + while (c) return x; + return y * y; + } + function f2(y) { + while (y) return 7; + return y * y + } + function f3(y) { + var n = 5 - y; + while (y) return n; + return y * y; + } + } +} + +collapse_vars_do_while: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(y) { + // The constant do-while condition `c` will be replaced. + var c = 9; + do { } while (c === 77); + } + function f2(y) { + // The non-constant do-while condition `c` will not be replaced. + var c = 5 - y; + do { } while (c); + } + function f3(y) { + // The constant `x` will be replaced in the do loop body. + function fn(n) { console.log(n); } + var a = 2, x = 7; + do { + fn(a = x); + break; + } while (y); + } + function f4(y) { + // The non-constant `a` will not be replaced in the do loop body. + var a = y / 4; + do { + return a; + } while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + // The non-constant `a` will be replaced in p(a) + // because it is declared in same block. + var a = y - 3; + p(a); + } while (--y); + } + } + expect: { + function f1(y) { + do ; while (false); + } + function f2(y) { + var c = 5 - y; + do ; while (c); + } + function f3(y) { + function fn(n) { console.log(n); } + var a = 2; + do { + fn(a = 7); + break; + } while (y); + } + function f4(y) { + var a = y / 4; + do + return a; + while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + p(y - 3); + } while (--y); + } + } +} + +collapse_vars_seq: { + 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; + return a + b; + }; + } + expect: { + var f1 = function(x, y) { + var a, b, r = x + y; + return a = r * r - r, b = 7, a + b + }; + } +} + +collapse_vars_throw: { + 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; + throw a + b; + }; + } + expect: { + var f1 = function(x, y) { + var a, b, r = x + y; + throw a = r * r - r, b = 7, a + b + }; + } +} + +collapse_vars_switch: { + 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: { + function f1() { + var not_used = sideeffect(), x = g1 + g2; + var y = x / 4, z = 'Bar' + y; + switch (z) { case 0: return g9; } + } + function f2() { + var x = g1 + g2, not_used = sideeffect(); + var y = x / 4 + var z = 'Bar' + y; + switch (z) { case 0: return g9; } + } + function f3(x) { + switch(x) { case 1: var a = 3 - x; return a; } + } + } + expect: { + function f1() { + sideeffect(); + switch ("Bar" + (g1 + g2) / 4) { case 0: return g9 } + } + function f2() { + var x = g1 + g2; + sideeffect(); + switch ("Bar" + x / 4) { case 0: return g9 } + } + function f3(x) { + // verify no extraneous semicolon in case block before return + // when the var definition was eliminated + switch(x) { case 1: return 3 - x; } + } + } +} + +collapse_vars_assignment: { + 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: { + function log(x) { return console.log(x), x; } + function f0(c) { + var a = 3 / c; + return a = a; + } + function f1(c) { + const a = 3 / c; + const b = 1 - a; + return b; + } + function f2(c) { + var a = 3 / c; + var b = a - 7; + return log(c = b); + } + function f3(c) { + var a = 3 / c; + var b = a - 7; + return log(c |= b); + } + function f4(c) { + var a = 3 / c; + var b = 2; + return log(b += a); + } + function f5(c) { + var b = 2; + var a = 3 / c; + return log(b += a); + } + function f6(c) { + var b = g(); + var a = 3 / c; + return log(b += a); + } + } + expect: { + function log(x) { return console.log(x), x; } + function f0(c) { + var a = 3 / c; + return a = a; + } + function f1(c) { + return 1 - 3 / c + } + function f2(c) { + return log(c = 3 / c - 7); + } + function f3(c) { + return log(c |= 3 / c - 7); + } + function f4(c) { + var b = 2; + return log(b += 3 / c); + } + function f5(c) { + var b = 2; + return log(b += 3 / c); + } + function f6(c) { + var b = g(); + return log(b += 3 / c); + } + } +} + +collapse_vars_lvalues: { + 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: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + function f3(x) { var a = (x -= 3), b = x + a; return b; } + function f4(x) { var a = (x -= 3); return x + a; } + function f5(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return b - c; } + function f6(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return c - b; } + function f7(x) { var w = e1(), v = e2(), c = v - x, b = w = x; return b - c; } + function f8(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return b - c; } + function f9(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return c - b; } + } + expect: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + 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 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 f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } + function f9(x) { var w = e1(); return e2() - x - (w = x); } + + } +} + +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 + } + input: { + function f0(o, a, h) { + var b = 3 - a; + var obj = o; + var seven = 7; + var prop = 'run'; + var t = obj[prop](b)[seven] = h; + return t; + } + function f1(x) { var y = 5 - x; return y; } + function f2(x) { const z = foo(), y = z / (5 - x); return y; } + function f3(x) { var z = foo(), y = (5 - x) / z; return y; } + function f4(x) { var z = foo(), y = (5 - u) / z; return y; } + function f5(x) { const z = foo(), y = (5 - window.x) / z; return y; } + function f6() { var b = window.a * window.z; return b && zap(); } + function f7() { var b = window.a * window.z; return b + b; } + function f8() { var b = window.a * window.z; var c = b + 5; return b + c; } + function f9() { var b = window.a * window.z; return bar() || b; } + function f10(x) { var a = 5, b = 3; return a += b; } + function f11(x) { var a = 5, b = 3; return a += --b; } + } + expect: { + function f0(o, a, h) { + var b = 3 - a; + return o.run(b)[7] = h; + } + function f1(x) { return 5 - x } + function f2(x) { return foo() / (5 - x) } + function f3(x) { return (5 - x) / foo() } + function f4(x) { var z = foo(); return (5 - u) / z } + function f5(x) { const z = foo(); return (5 - window.x) / z } + function f6() { return window.a * window.z && zap() } + function f7() { var b = window.a * window.z; return b + b } + function f8() { var b = window.a * window.z; return b + (b + 5) } + function f9() { var b = window.a * window.z; return bar() || b } + function f10(x) { var a = 5; return a += 3; } + function f11(x) { var a = 5, b = 3; return a += --b; } + } +} + +collapse_vars_self_reference: { + options = { + collapse_vars:true, unused:false, + sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + // avoid bug in self-referential declaration. + function f1() { + var self = { + inner: function() { return self; } + }; + } + function f2() { + var self = { inner: self }; + } + } + expect: { + // note: `unused` option is false + function f1() { + var self = { + inner: function() { return self } + }; + } + function f2() { + var self = { inner: self }; + } + } +} + +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 + } + input: { + function f1() { + var dummy = 3, a = 5, unused = 2, a = 1, a = 3; + return -a; + } + function f2(x) { + var a = 3, a = x; + return a; + } + (function(x){ + var a = "GOOD" + x, e = "BAD", k = "!", e = a; + console.log(e + k); + })("!"), + + (function(x){ + var a = "GOOD" + x, e = "BAD" + x, k = "!", e = a; + console.log(e + k); + })("!"); + } + expect: { + function f1() { + return -3 + } + function f2(x) { + return x + } + (function(x){ + var a = "GOOD" + x, e = "BAD", e = a; + console.log(e + "!"); + })("!"), + (function(x){ + var a = "GOOD" + x, e = "BAD" + x, e = a; + console.log(e + "!"); + })("!"); + } +} + +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 + } + input: { + function constant_vars_can_be_replaced_in_any_scope() { + var outer = 3; + return function() { return outer; } + } + function non_constant_vars_can_only_be_replace_in_same_scope(x) { + var outer = x; + return function() { return outer; } + } + } + expect: { + function constant_vars_can_be_replaced_in_any_scope() { + return function() { return 3 } + } + function non_constant_vars_can_only_be_replace_in_same_scope(x) { + var outer = x + return function() { return outer } + } + } +} + +collapse_vars_unary: { + 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: { + function f0(o, p) { + var x = o[p]; + delete x; + } + function f1(n) { + var k = !!n; + return n > +k; + } + function f2(n) { + // test unary with constant + var k = 7; + return k--; + } + function f3(n) { + // test unary with constant + var k = 7; + return ++k; + } + function f4(n) { + // test unary with non-constant + var k = 8 - n; + return k--; + } + function f5(n) { + // test unary with non-constant + var k = 9 - n; + return ++k; + } + } + expect: { + function f0(o, p) { + delete o[p]; + } + function f1(n) { + return n > +!!n + } + function f2(n) { + var k = 7; + return k-- + } + function f3(n) { + var k = 7; + return ++k + } + function f4(n) { + var k = 8 - n; + return k--; + } + function f5(n) { + var k = 9 - n; + return ++k; + } + } +} + +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 + } + input: { + function f1() { + try { + var a = 1; + return a; + } + catch (ex) { + var b = 2; + return b; + } + finally { + var c = 3; + return c; + } + } + function f2() { + var t = could_throw(); // shouldn't be replaced in try block + try { + return t + might_throw(); + } + catch (ex) { + return 3; + } + } + } + expect: { + function f1() { + try { + return 1; + } + catch (ex) { + return 2; + } + finally { + return 3; + } + } + function f2() { + var t = could_throw(); + try { + return t + might_throw(); + } + catch (ex) { + return 3; + } + } + } +} + +collapse_vars_array: { + 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: { + function f1(x, y) { + var z = x + y; + return [z]; + } + function f2(x, y) { + var z = x + y; + return [x, side_effect(), z]; + } + function f3(x, y) { + var z = f(x + y); + return [ [3], [z, x, y], [g()] ]; + } + } + expect: { + function f1(x, y) { + return [x + y] + } + function f2(x, y) { + var z = x + y + return [x, side_effect(), z] + } + function f3(x, y) { + return [ [3], [f(x + y), x, y], [g()] ] + } + } +} + +collapse_vars_object: { + 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: { + function f0(x, y) { + var z = x + y; + return { + get b() { return 7; }, + r: z + }; + } + function f1(x, y) { + var z = x + y; + return { + r: z, + get b() { return 7; } + }; + } + function f2(x, y) { + var z = x + y; + var k = x - y; + return { + q: k, + r: g(x), + s: z + }; + } + function f3(x, y) { + var z = f(x + y); + return [{ + a: {q: x, r: y, s: z}, + b: g() + }]; + } + } + expect: { + function f0(x, y) { + var z = x + y; + return { + get b() { return 7; }, + r: z + }; + } + function f1(x, y) { + return { + r: x + y, + get b() { return 7; } + }; + } + function f2(x, y) { + var z = x + y; + return { + q: x - y, + r: g(x), + s: z + }; + } + function f3(x, y) { + return [{ + a: {q: x, r: y, s: f(x + y)}, + b: g() + }]; + } + } +} + +collapse_vars_eval_and_with: { + options = { + collapse_vars:true, sequences:false, 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: { + // Don't attempt to collapse vars in presence of eval() or with statement. + (function f0() { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(); + (function f1() { + var o = {a: 1}, a = 2; + with (o) console.log(a); + })(); + (function f2() { + var o = {a: 1}, a = 2; + return function() { with (o) console.log(a) }; + })()(); + } + expect: { + (function f0() { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(); + (function f1() { + var o = {a: 1}, a = 2; + with(o) console.log(a); + })(); + (function f2() { + var o = {a: 1}, a = 2; + return function() { with (o) console.log(a) }; + })()(); + } +} + +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 + } + input: { + function f1(x) { + var a = 4, b = x.prop, c = 5, d = sideeffect1(), e = sideeffect2(); + return b + (function() { return d - a * e - c; })(); + } + function f2(x) { + var a = 4, b = x.prop, c = 5, not_used = sideeffect1(), e = sideeffect2(); + return b + (function() { return -a * e - c; })(); + } + function f3(x) { + var a = 4, b = x.prop, c = 5, not_used = sideeffect1(); + return b + (function() { return -a - c; })(); + } + } + expect: { + function f1(x) { + var b = x.prop, d = sideeffect1(), e = sideeffect2(); + return b + (function() { return d - 4 * e - 5; })(); + } + function f2(x) { + var b = x.prop, e = (sideeffect1(), sideeffect2()); + return b + (function() { return -4 * e - 5; })(); + } + function f3(x) { + var b = x.prop; + sideeffect1(); + return b + (function() { return -9; })(); + } + } +} + +collapse_vars_arguments: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + var outer = function() { + // Do not replace `arguments` but do replace the constant `k` before it. + var k = 7, arguments = 5, inner = function() { console.log(arguments); } + inner(k, 1); + } + outer(); + } + expect: { + (function() { + (function(){console.log(arguments);})(7, 1); + })(); + } +} + +collapse_vars_short_circuit: { + 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: { + function f0(x) { var a = foo(), b = bar(); return b || x; } + function f1(x) { var a = foo(), b = bar(); return b && x; } + function f2(x) { var a = foo(), b = bar(); return x && a && b; } + function f3(x) { var a = foo(), b = bar(); return a && x; } + function f4(x) { var a = foo(), b = bar(); return a && x && b; } + function f5(x) { var a = foo(), b = bar(); return x || a || b; } + function f6(x) { var a = foo(), b = bar(); return a || x || b; } + function f7(x) { var a = foo(), b = bar(); return a && b && x; } + function f8(x,y) { var a = foo(), b = bar(); return (x || a) && (y || b); } + function f9(x,y) { var a = foo(), b = bar(); return (x && a) || (y && b); } + function f10(x,y) { var a = foo(), b = bar(); return (x - a) || (y - b); } + function f11(x,y) { var a = foo(), b = bar(); return (x - b) || (y - a); } + function f12(x,y) { var a = foo(), b = bar(); return (x - y) || (b - a); } + function f13(x,y) { var a = foo(), b = bar(); return (a - b) || (x - y); } + function f14(x,y) { var a = foo(), b = bar(); return (b - a) || (x - y); } + } + expect: { + function f0(x) { foo(); return bar() || x; } + function f1(x) { foo(); return bar() && x; } + function f2(x) { var a = foo(), b = bar(); return x && a && b; } + function f3(x) { var a = foo(); bar(); return a && x; } + function f4(x) { var a = foo(), b = bar(); return a && x && b; } + function f5(x) { var a = foo(), b = bar(); return x || a || b; } + function f6(x) { var a = foo(), b = bar(); return a || x || b; } + function f7(x) { var a = foo(), b = bar(); return a && b && x; } + function f8(x,y) { var a = foo(), b = bar(); return (x || a) && (y || b); } + function f9(x,y) { var a = foo(), b = bar(); return (x && a) || (y && b); } + function f10(x,y) { var a = foo(), b = bar(); return (x - a) || (y - b); } + function f11(x,y) { var a = foo(); return (x - bar()) || (y - a); } + function f12(x,y) { var a = foo(), b = bar(); return (x - y) || (b - a); } + function f13(x,y) { return (foo() - bar()) || (x - y); } + function f14(x,y) { var a = foo(); return (bar() - a) || (x - y); } + } +} + +collapse_vars_short_circuited_conditions: { + options = { + collapse_vars: true, + sequences: false, + dead_code: true, + conditionals: false, + comparisons: false, + evaluate: true, + booleans: true, + loops: true, + unused: true, + hoist_funs: true, + keep_fargs: true, + if_return: false, + join_vars: true, + cascade: true, + side_effects: true, + } + input: { + function c1(x) { var a = foo(), b = bar(), c = baz(); return a ? b : c; } + function c2(x) { var a = foo(), b = bar(), c = baz(); return a ? c : b; } + function c3(x) { var a = foo(), b = bar(), c = baz(); return b ? a : c; } + function c4(x) { var a = foo(), b = bar(), c = baz(); return b ? c : a; } + function c5(x) { var a = foo(), b = bar(), c = baz(); return c ? a : b; } + function c6(x) { var a = foo(), b = bar(), c = baz(); return c ? b : a; } + + function i1(x) { var a = foo(), b = bar(), c = baz(); if (a) return b; else return c; } + function i2(x) { var a = foo(), b = bar(), c = baz(); if (a) return c; else return b; } + function i3(x) { var a = foo(), b = bar(), c = baz(); if (b) return a; else return c; } + function i4(x) { var a = foo(), b = bar(), c = baz(); if (b) return c; else return a; } + function i5(x) { var a = foo(), b = bar(), c = baz(); if (c) return a; else return b; } + function i6(x) { var a = foo(), b = bar(), c = baz(); if (c) return b; else return a; } + } + expect: { + function c1(x) { var a = foo(), b = bar(), c = baz(); return a ? b : c; } + function c2(x) { var a = foo(), b = bar(), c = baz(); return a ? c : b; } + function c3(x) { var a = foo(), b = bar(), c = baz(); return b ? a : c; } + function c4(x) { var a = foo(), b = bar(), c = baz(); return b ? c : a; } + function c5(x) { var a = foo(), b = bar(); return baz() ? a : b; } + function c6(x) { var a = foo(), b = bar(); return baz() ? b : a; } + + function i1(x) { var a = foo(), b = bar(), c = baz(); if (a) return b; else return c; } + function i2(x) { var a = foo(), b = bar(), c = baz(); if (a) return c; else return b; } + function i3(x) { var a = foo(), b = bar(), c = baz(); if (b) return a; else return c; } + function i4(x) { var a = foo(), b = bar(), c = baz(); if (b) return c; else return a; } + function i5(x) { var a = foo(), b = bar(); if (baz()) return a; else return b; } + function i6(x) { var a = foo(), b = bar(); if (baz()) return b; else return a; } + } +} + diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 65cfea64..f5eeb6f2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -407,8 +407,8 @@ cond_8: { a = !condition; a = !condition; - a = condition ? 1 : false; - a = condition ? 0 : true; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } @@ -490,8 +490,8 @@ cond_8b: { a = !condition; a = !condition; - a = condition ? 1 : !1; - a = condition ? 0 : !0; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } @@ -557,7 +557,7 @@ cond_8c: { a = !!condition; a = !condition; - a = condition() ? !0 : !-3.5; + a = !!condition() || !-3.5; a = !!condition; a = !!condition; @@ -573,12 +573,68 @@ cond_8c: { a = !condition; a = !condition; - a = condition ? 1 : false; - a = condition ? 0 : true; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } +ternary_boolean_consequent: { + 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: { + function f1() { return a == b ? true : x; } + function f2() { return a == b ? false : x; } + function f3() { return a < b ? !0 : x; } + function f4() { return a < b ? !1 : x; } + function f5() { return c ? !0 : x; } + function f6() { return c ? false : x; } + function f7() { return !c ? true : x; } + function f8() { return !c ? !1 : x; } + } + expect: { + function f1() { return a == b || x; } + function f2() { return a != b && x; } + function f3() { return a < b || x; } + function f4() { return !(a < b) && x; } + function f5() { return !!c || x; } + function f6() { return !c && x; } + function f7() { return !c || x; } + function f8() { return !!c && x; } + } +} + +ternary_boolean_alternative: { + 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: { + function f1() { return a == b ? x : true; } + function f2() { return a == b ? x : false; } + function f3() { return a < b ? x : !0; } + function f4() { return a < b ? x : !1; } + function f5() { return c ? x : true; } + function f6() { return c ? x : !1; } + function f7() { return !c ? x : !0; } + function f8() { return !c ? x : false; } + } + expect: { + function f1() { return a != b || x; } + function f2() { return a == b && x; } + function f3() { return !(a < b) || x; } + function f4() { return a < b && x; } + function f5() { return !c || x; } + function f6() { return !!c && x; } + function f7() { return !!c || x; } + function f8() { return !c && x; } + } +} + conditional_and: { options = { conditionals: true, @@ -738,3 +794,77 @@ conditional_or: { a = condition + 3 || null; } } + +trivial_boolean_ternary_expressions : { + options = { + conditionals: true, + evaluate : true, + booleans : true + }; + input: { + f('foo' in m ? true : false); + f('foo' in m ? false : true); + + f(g ? true : false); + f(foo() ? true : false); + f("bar" ? true : false); + f(5 ? true : false); + f(5.7 ? true : false); + f(x - y ? true : false); + + f(x == y ? true : false); + f(x === y ? !0 : !1); + f(x < y ? !0 : false); + f(x <= y ? true : false); + f(x > y ? true : !1); + f(x >= y ? !0 : !1); + + f(g ? false : true); + f(foo() ? false : true); + f("bar" ? false : true); + f(5 ? false : true); + f(5.7 ? false : true); + f(x - y ? false : true); + + f(x == y ? !1 : !0); + f(x === y ? false : true); + + f(x < y ? false : true); + f(x <= y ? false : !0); + f(x > y ? !1 : true); + f(x >= y ? !1 : !0); + } + expect: { + f('foo' in m); + f(!('foo' in m)); + + f(!!g); + f(!!foo()); + f(!0); + f(!0); + f(!0); + f(!!(x - y)); + + f(x == y); + f(x === y); + f(x < y); + f(x <= y); + f(x > y); + f(x >= y); + + f(!g); + f(!foo()); + f(!1); + f(!1); + f(!1); + f(!(x - y)); + + f(x != y); + f(x !== y); + + f(!(x < y)); + f(!(x <= y)); + f(!(x > y)); + f(!(x >= y)); + } +} diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index dddc9159..652645d9 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -87,8 +87,8 @@ dead_code_constant_boolean_should_warn_more: { var moo; } } - -dead_code_block_decls_die: { + +dead_code_block_decls_die: { options = { dead_code : true, conditionals : true, @@ -109,3 +109,120 @@ dead_code_block_decls_die: { console.log(foo, bar, Baz); } } + +dead_code_const_declaration: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + const CONST_FOO = false; + if (CONST_FOO) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + const CONST_FOO = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + /** @const */ var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation_regex: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + CONST_FOO_ANN && console.log('reachable'); + } +} + +dead_code_const_annotation_complex_scope: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused_var; + /** @const */ var test = 'test'; + // @const + var CONST_FOO_ANN = false; + var unused_var_2; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + if (test === 'test') { + var beef = 'good'; + /** @const */ var meat = 'beef'; + var pork = 'bad'; + if (meat === 'pork') { + console.log('also unreachable'); + } else if (pork === 'good') { + console.log('reached, not const'); + } + } + } + expect: { + var unused_var; + var test = 'test'; + var CONST_FOO_ANN = !1; + var unused_var_2; + var moo; + function bar() {} + var beef = 'good'; + var meat = 'beef'; + var pork = 'bad'; + 'good' === pork && console.log('reached, not const'); + } +} diff --git a/test/compress/issue-1034.js b/test/compress/issue-1034.js new file mode 100644 index 00000000..039af2ed --- /dev/null +++ b/test/compress/issue-1034.js @@ -0,0 +1,137 @@ +non_hoisted_function_after_return: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true + } + input: { + function foo(x) { + if (x) { + return bar(); + not_called1(); + } else { + return baz(); + not_called2(); + } + function bar() { return 7; } + return not_reached; + function UnusedFunction() {} + function baz() { return 8; } + } + } + expect: { + function foo(x) { + return x ? bar() : baz(); + function bar() { return 7 } + function baz() { return 8 } + } + } + expect_warnings: [ + 'WARN: Dropping unreachable code [test/compress/issue-1034.js:11,16]', + "WARN: Dropping unreachable code [test/compress/issue-1034.js:14,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:17,12]", + "WARN: Dropping unused function UnusedFunction [test/compress/issue-1034.js:18,21]" + ] +} + +non_hoisted_function_after_return_2a: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true, + collapse_vars: false + } + input: { + function foo(x) { + if (x) { + return bar(1); + var a = not_called(1); + } else { + return bar(2); + var b = not_called(2); + } + var c = bar(3); + function bar(x) { return 7 - x; } + function nope() {} + return b || c; + } + } + expect: { + // NOTE: Output is correct, but suboptimal. Not a regression. Can be improved in future. + // This output is run through non_hoisted_function_after_return_2b with same flags. + function foo(x) { + if (x) { + return bar(1); + } else { + return bar(2); + var b; + } + var c = bar(3); + function bar(x) { + return 7 - x; + } + return b || c; + } + } + expect_warnings: [ + "WARN: Dropping unreachable code [test/compress/issue-1034.js:48,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:48,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:51,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:51,16]", + "WARN: Dropping unused variable a [test/compress/issue-1034.js:48,20]", + "WARN: Dropping unused function nope [test/compress/issue-1034.js:55,21]" + ] +} + +non_hoisted_function_after_return_2b: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true, + collapse_vars: false + } + input: { + // Note: output of test non_hoisted_function_after_return_2a run through compress again + function foo(x) { + if (x) { + return bar(1); + } else { + return bar(2); + var b; + } + var c = bar(3); + function bar(x) { + return 7 - x; + } + return b || c; + } + } + expect: { + // the output we would have liked to see from non_hoisted_function_after_return_2a + function foo(x) { + return bar(x ? 1 : 2); + function bar(x) { return 7 - x; } + } + } + expect_warnings: [ + // Notice that some warnings are repeated by multiple compress passes. + // Not a regression. There is room for improvement here. + // Warnings should be cached and only output if unique. + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:102,12]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:102,12]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:106,12]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unused variable b [test/compress/issue-1034.js:100,20]", + "WARN: Dropping unused variable c [test/compress/issue-1034.js:102,16]" + ] +} + diff --git a/test/compress/issue-1041.js b/test/compress/issue-1041.js new file mode 100644 index 00000000..9dd176fd --- /dev/null +++ b/test/compress/issue-1041.js @@ -0,0 +1,39 @@ +const_declaration: { + options = { + evaluate: true + }; + + input: { + const goog = goog || {}; + } + expect: { + const goog = goog || {}; + } +} + +const_pragma: { + options = { + evaluate: true + }; + + input: { + /** @const */ var goog = goog || {}; + } + expect: { + var goog = goog || {}; + } +} + +// for completeness' sake +not_const: { + options = { + evaluate: true + }; + + input: { + var goog = goog || {}; + } + expect: { + var goog = goog || {}; + } +} diff --git a/test/compress/issue-12.js b/test/compress/issue-12.js index bf87d5c0..e2d8bda7 100644 --- a/test/compress/issue-12.js +++ b/test/compress/issue-12.js @@ -9,3 +9,50 @@ keep_name_of_setter: { input: { a = { set foo () {} } } expect: { a = { set foo () {} } } } + +setter_with_operator_keys: { + input: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } + expect: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } +} \ No newline at end of file diff --git a/test/compress/issue-782.js b/test/compress/issue-782.js index cce15fd1..2f72d1ab 100644 --- a/test/compress/issue-782.js +++ b/test/compress/issue-782.js @@ -1,23 +1,27 @@ remove_redundant_sequence_items: { options = { side_effects: true }; input: { + (0, 1, eval)(); (0, 1, logThis)(); (0, 1, _decorators.logThis)(); } expect: { - (0, logThis)(); + (0, eval)(); + logThis(); (0, _decorators.logThis)(); } } -dont_remove_lexical_binding_sequence: { +dont_remove_this_binding_sequence: { options = { side_effects: true }; input: { + (0, eval)(); (0, logThis)(); (0, _decorators.logThis)(); } expect: { - (0, logThis)(); + (0, eval)(); + logThis(); (0, _decorators.logThis)(); } } diff --git a/test/compress/issue-892.js b/test/compress/issue-892.js new file mode 100644 index 00000000..2dab420f --- /dev/null +++ b/test/compress/issue-892.js @@ -0,0 +1,32 @@ +dont_mangle_arguments: { + mangle = { + }; + options = { + sequences : true, + properties : true, + dead_code : true, + drop_debugger : true, + conditionals : true, + comparisons : true, + evaluate : true, + booleans : true, + loops : true, + unused : true, + hoist_funs : true, + keep_fargs : true, + keep_fnames : false, + hoist_vars : true, + if_return : true, + join_vars : true, + cascade : true, + side_effects : true, + negate_iife : false + }; + input: { + (function(){ + var arguments = arguments, not_arguments = 9; + console.log(not_arguments, arguments); + })(5,6,7); + } + expect_exact: "(function(){var arguments=arguments,o=9;console.log(o,arguments)})(5,6,7);" +} diff --git a/test/compress/issue-913.js b/test/compress/issue-913.js new file mode 100644 index 00000000..9d34d9d9 --- /dev/null +++ b/test/compress/issue-913.js @@ -0,0 +1,20 @@ +keep_var_for_in: { + options = { + hoist_vars: true, + unused: true + }; + input: { + (function(obj){ + var foo = 5; + for (var i in obj) + return foo; + })(); + } + expect: { + (function(obj){ + var i, foo = 5; + for (i in obj) + return foo; + })(); + } +} diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js new file mode 100644 index 00000000..0e040922 --- /dev/null +++ b/test/compress/issue-973.js @@ -0,0 +1,96 @@ +this_binding_conditionals: { + options = { + conditionals: true, + evaluate : true + }; + input: { + (1 && a)(); + (0 || a)(); + (0 || 1 && a)(); + (1 ? a : 0)(); + + (1 && a.b)(); + (0 || a.b)(); + (0 || 1 && a.b)(); + (1 ? a.b : 0)(); + + (1 && a[b])(); + (0 || a[b])(); + (0 || 1 && a[b])(); + (1 ? a[b] : 0)(); + + (1 && eval)(); + (0 || eval)(); + (0 || 1 && eval)(); + (1 ? eval : 0)(); + } + expect: { + a(); + a(); + a(); + a(); + + (0, a.b)(); + (0, a.b)(); + (0, a.b)(); + (0, a.b)(); + + (0, a[b])(); + (0, a[b])(); + (0, a[b])(); + (0, a[b])(); + + (0, eval)(); + (0, eval)(); + (0, eval)(); + (0, eval)(); + } +} + +this_binding_collapse_vars: { + options = { + collapse_vars: true, + }; + input: { + var c = a; c(); + var d = a.b; d(); + var e = eval; e(); + } + expect: { + a(); + (0, a.b)(); + (0, eval)(); + } +} + +this_binding_side_effects: { + options = { + side_effects : true + }; + input: { + (function (foo) { + (0, foo)(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + (function (foo) { + var eval = console; + (0, foo)(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + } + expect: { + (function (foo) { + foo(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + (function (foo) { + var eval = console; + foo(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + } +} \ No newline at end of file diff --git a/test/compress/issue-976.js b/test/compress/issue-976.js new file mode 100644 index 00000000..06e11f40 --- /dev/null +++ b/test/compress/issue-976.js @@ -0,0 +1,88 @@ +eval_collapse_vars: { + options = { + collapse_vars:true, sequences:false, 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: { + function f1() { + var e = 7; + var s = "abcdef"; + var i = 2; + var eval = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + eval(x, y, z, e); + } + function p1() { var a = foo(), b = bar(), eval = baz(); return a + b + eval; } + function p2() { var a = foo(), b = bar(), eval = baz; return a + b + eval(); } + (function f2(eval) { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(eval); + } + expect: { + function f1() { + var e = 7, + s = "abcdef", + i = 2, + eval = console.log.bind(console), + x = s.charAt(i++), + y = s.charAt(i++), + z = s.charAt(i++); + eval(x, y, z, e); + } + function p1() { return foo() + bar() + baz(); } + function p2() { var a = foo(), b = bar(), eval = baz; return a + b + eval(); } + (function f2(eval) { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(eval); + } +} + +eval_unused: { + options = { unused: true, keep_fargs: false }; + input: { + function f1(a, eval, c, d, e) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } + expect: { + function f1(a, eval) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } +} + +eval_mangle: { + mangle = { + }; + input: { + function f1(a, eval, c, d, e) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } + expect_exact: 'function f1(n,c,e,a,f){return n("c")+c}function f2(a,b,c,d,e){return a+eval("c")}function f3(a,eval,c,d,e){return a+eval("c")}' +} diff --git a/test/compress/issue-979.js b/test/compress/issue-979.js new file mode 100644 index 00000000..bae15db8 --- /dev/null +++ b/test/compress/issue-979.js @@ -0,0 +1,89 @@ +issue979_reported: { + options = { + 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: { + function f1() { + if (a == 1 || b == 2) { + foo(); + } + } + function f2() { + if (!(a == 1 || b == 2)) { + } + else { + foo(); + } + } + } + expect: { + function f1() { + 1!=a&&2!=b||foo(); + } + function f2() { + 1!=a&&2!=b||foo(); + } + } +} + +issue979_test_negated_is_best: { + options = { + 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: { + function f3() { + if (a == 1 | b == 2) { + foo(); + } + } + function f4() { + if (!(a == 1 | b == 2)) { + } + else { + foo(); + } + } + function f5() { + if (a == 1 && b == 2) { + foo(); + } + } + function f6() { + if (!(a == 1 && b == 2)) { + } + else { + foo(); + } + } + function f7() { + if (a == 1 || b == 2) { + foo(); + } + else { + return bar(); + } + } + } + expect: { + function f3() { + 1==a|2==b&&foo(); + } + function f4() { + 1==a|2==b&&foo(); + } + function f5() { + 1==a&&2==b&&foo(); + } + function f6() { + 1!=a||2!=b||foo(); + } + function f7() { + return 1!=a&&2!=b?bar():void foo(); + } + } +} + diff --git a/test/compress/numbers.js b/test/compress/numbers.js new file mode 100644 index 00000000..8e32ad02 --- /dev/null +++ b/test/compress/numbers.js @@ -0,0 +1,19 @@ +hex_numbers_in_parentheses_for_prototype_functions: { + input: { + (-2); + (-2).toFixed(0); + + (2); + (2).toFixed(0); + + (0.2); + (0.2).toFixed(0); + + (0.00000002); + (0.00000002).toFixed(0); + + (1000000000000000128); + (1000000000000000128).toFixed(0); + } + expect_exact: "-2;(-2).toFixed(0);2;2..toFixed(0);.2;.2.toFixed(0);2e-8;2e-8.toFixed(0);0xde0b6b3a7640080;(0xde0b6b3a7640080).toFixed(0);" +} diff --git a/test/mocha.js b/test/mocha.js new file mode 100644 index 00000000..411f52c5 --- /dev/null +++ b/test/mocha.js @@ -0,0 +1,29 @@ +var Mocha = require('mocha'), + fs = require('fs'), + path = require('path'); + +// Instantiate a Mocha instance. +var mocha = new Mocha({}); + +var testDir = __dirname + '/mocha/'; + +// Add each .js file to the mocha instance +fs.readdirSync(testDir).filter(function(file){ + // Only keep the .js files + return file.substr(-3) === '.js'; + +}).forEach(function(file){ + mocha.addFile( + path.join(testDir, file) + ); +}); + +module.exports = function() { + mocha.run(function(failures) { + if (failures !== 0) { + process.on('exit', function () { + process.exit(failures); + }); + } + }); +}; \ No newline at end of file diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js new file mode 100644 index 00000000..089826fc --- /dev/null +++ b/test/mocha/arguments.js @@ -0,0 +1,22 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("arguments", function() { + it("Should known that arguments in functions are local scoped", function() { + var ast = UglifyJS.parse("var arguments; var f = function() {arguments.length}"); + ast.figure_out_scope(); + + // Test scope of `var arguments` + assert.strictEqual(ast.find_variable("arguments").global, true); + + // Select arguments symbol in function + var symbol = ast.body[1].definitions[0].value.find_variable("arguments"); + + assert.strictEqual(symbol.global, false); + assert.strictEqual(symbol.scope, ast. // From ast + body[1]. // Select 2nd statement (equals to `var f ...`) + definitions[0]. // First definition of selected statement + value // Select function as scope + ); + }); +}); \ No newline at end of file diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js new file mode 100644 index 00000000..ea2ec2eb --- /dev/null +++ b/test/mocha/comment-filter.js @@ -0,0 +1,45 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("comment filters", function() { + it("Should be able to filter comments by passing regex", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + assert.strictEqual(ast.print_to_string({comments: /^!/}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n"); + }); + + it("Should be able to filter comments by passing a function", function() { + var ast = UglifyJS.parse("/*TEST 123*/\n//An other comment\n//8 chars."); + var f = function(node, comment) { + return comment.value.length === 8; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "/*TEST 123*/\n//8 chars.\n"); + }); + + it("Should be able to get the comment and comment type when using a function", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + var f = function(node, comment) { + return comment.type == "comment1" || comment.type == "comment3"; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "//!test3\n//test4\n//test5\n//!test6\n"); + }); + + it("Should be able to filter comments by passing a boolean", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + + assert.strictEqual(ast.print_to_string({comments: true}), "/*!test1*/\n/*test2*/\n//!test3\n//test4\n//test5\n//!test6\n//test7\n//!test8\n"); + assert.strictEqual(ast.print_to_string({comments: false}), ""); + }); + + it("Should never be able to filter comment5 (shebangs)", function() { + var ast = UglifyJS.parse("#!Random comment\n//test1\n/*test2*/"); + var f = function(node, comment) { + assert.strictEqual(comment.type === "comment5", false); + + return true; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/\n"); + }); +}); diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js new file mode 100644 index 00000000..641a2026 --- /dev/null +++ b/test/mocha/getter-setter.js @@ -0,0 +1,89 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("Getters and setters", function() { + it("Should not accept operator symbols as getter/setter name", function() { + var illegalOperators = [ + "++", + "--", + "+", + "-", + "!", + "~", + "&", + "|", + "^", + "*", + "/", + "%", + ">>", + "<<", + ">>>", + "<", + ">", + "<=", + ">=", + "==", + "===", + "!=", + "!==", + "?", + "=", + "+=", + "-=", + "/=", + "*=", + "%=", + ">>=", + "<<=", + ">>>=", + "|=", + "^=", + "&=", + "&&", + "||" + ]; + var generator = function() { + var results = []; + + for (var i in illegalOperators) { + results.push({ + code: "var obj = { get " + illegalOperators[i] + "() { return test; }};", + operator: illegalOperators[i], + method: "get" + }); + results.push({ + code: "var obj = { set " + illegalOperators[i] + "(value) { test = value}};", + operator: illegalOperators[i], + method: "set" + }); + } + + return results; + }; + + var testCase = function(data) { + return function() { + UglifyJS.parse(data.code); + }; + }; + + var fail = function(data) { + return function (e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Invalid getter/setter name: " + data.operator; + }; + }; + + var errorMessage = function(data) { + return "Expected but didn't get a syntax error while parsing following line:\n" + data.code; + }; + + var tests = generator(); + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + assert.throws(testCase(test), fail(test), errorMessage(test)); + } + }); + +}); diff --git a/test/mocha/huge-number-of-comments.js b/test/mocha/huge-number-of-comments.js new file mode 100644 index 00000000..3b90bc0e --- /dev/null +++ b/test/mocha/huge-number-of-comments.js @@ -0,0 +1,19 @@ +var Uglify = require('../../'); +var assert = require("assert"); + +describe("Huge number of comments.", function() { + it("Should parse and compress code with thousands of consecutive comments", function() { + var js = 'function lots_of_comments(x) { return 7 -'; + var i; + 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: {} + }); + assert.strictEqual(result.code, "function lots_of_comments(x){return 7-x}"); + }); +}); + diff --git a/test/mocha/let.js b/test/mocha/let.js new file mode 100644 index 00000000..89fd9f1a --- /dev/null +++ b/test/mocha/let.js @@ -0,0 +1,30 @@ +var Uglify = require('../../'); +var assert = require("assert"); + +describe("let", function() { + it("Should not produce `let` as a variable name in mangle", function(done) { + this.timeout(10000); + + // Produce a lot of variables in a function and run it through mangle. + var s = '"use strict"; function foo() {'; + for (var i = 0; i < 21000; ++i) { + s += "var v" + i + "=0;"; + } + s += '}'; + var result = Uglify.minify(s, {fromString: true, compress: false}); + + // Verify that select keywords and reserved keywords not produced + assert.strictEqual(result.code.indexOf("var let="), -1); + assert.strictEqual(result.code.indexOf("var do="), -1); + assert.strictEqual(result.code.indexOf("var var="), -1); + + // Verify that the variable names that appeared immediately before + // and after the erroneously generated `let` variable name still exist + // to show the test generated enough symbols. + assert(result.code.indexOf("var ket=") >= 0); + assert(result.code.indexOf("var met=") >= 0); + + done(); + }); +}); + diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js new file mode 100644 index 00000000..84aaad7e --- /dev/null +++ b/test/mocha/string-literal.js @@ -0,0 +1,34 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("String literals", function() { + it("Should throw syntax error if a string literal contains a newline", function() { + var inputs = [ + "'\n'", + "'\r'", + '"\r\n"', + "'\u2028'", + '"\u2029"' + ]; + + var test = function(input) { + return function() { + var ast = UglifyJS.parse(input); + }; + }; + + var error = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Unterminated string constant"; + }; + + for (var input in inputs) { + assert.throws(test(inputs[input]), error); + } + }); + + it("Should not throw syntax error if a string has a line continuation", function() { + var output = UglifyJS.parse('var a = "a\\\nb";').print_to_string(); + assert.equal(output, 'var a="ab";'); + }); +}); \ No newline at end of file diff --git a/test/run-tests.js b/test/run-tests.js index c1530109..538a75cd 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -16,6 +16,9 @@ if (failures) { process.exit(1); } +var mocha_tests = require("./mocha.js"); +mocha_tests(); + var run_sourcemaps_tests = require('./sourcemaps'); run_sourcemaps_tests(); @@ -86,9 +89,18 @@ function run_compress_tests() { log_start_file(file); function test_case(test) { log_test(test.name); + U.base54.reset(); var options = U.defaults(test.options, { warnings: false }); + var warnings_emitted = []; + var original_warn_function = U.AST_Node.warn_function; + if (test.expect_warnings) { + U.AST_Node.warn_function = function(text) { + warnings_emitted.push("WARN: " + text); + }; + options.warnings = true; + } var cmp = new U.Compressor(options, true); var output_options = test.beautify || {}; var expect; @@ -105,6 +117,7 @@ function run_compress_tests() { var output = input.transform(cmp); output.figure_out_scope(); if (test.mangle) { + output.compute_char_frequency(test.mangle); output.mangle_names(test.mangle); } output = make_code(output, output_options); @@ -117,6 +130,24 @@ function run_compress_tests() { failures++; failed_files[file] = 1; } + else if (test.expect_warnings) { + U.AST_Node.warn_function = original_warn_function; + var expected_warnings = make_code(test.expect_warnings, { + beautify: false, + quote_style: 2, // force double quote to match JSON + }); + var actual_warnings = JSON.stringify(warnings_emitted); + actual_warnings = actual_warnings.split(process.cwd() + "/").join(""); + if (expected_warnings != actual_warnings) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { + input: input_code, + expected_warnings: expected_warnings, + actual_warnings: actual_warnings, + }); + failures++; + failed_files[file] = 1; + } + } } var tests = parse_test(path.resolve(dir, file)); for (var i in tests) if (tests.hasOwnProperty(i)) { @@ -168,7 +199,7 @@ function parse_test(file) { } if (node instanceof U.AST_LabeledStatement) { assert.ok( - node.label.name == "input" || node.label.name == "expect" || node.label.name == "expect_exact", + ["input", "expect", "expect_exact", "expect_warnings"].indexOf(node.label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", { name: node.label.name, line: node.label.start.line, diff --git a/tools/exports.js b/tools/exports.js index 5007e03b..110b5c4e 100644 --- a/tools/exports.js +++ b/tools/exports.js @@ -15,3 +15,4 @@ exports["parse"] = parse; exports["push_uniq"] = push_uniq; exports["string_template"] = string_template; exports["is_identifier"] = is_identifier; +exports["SymbolDef"] = SymbolDef; diff --git a/tools/node.js b/tools/node.js index f6048661..fa8c19dc 100644 --- a/tools/node.js +++ b/tools/node.js @@ -32,15 +32,18 @@ UglifyJS.AST_Node.warn_function = function(txt) { exports.minify = function(files, options) { options = UglifyJS.defaults(options, { - spidermonkey : false, - outSourceMap : null, - sourceRoot : null, - inSourceMap : null, - fromString : false, - warnings : false, - mangle : {}, - output : null, - compress : {} + spidermonkey : false, + outSourceMap : null, + sourceRoot : null, + inSourceMap : null, + fromString : false, + warnings : false, + mangle : {}, + mangleProperties : false, + nameCache : null, + output : null, + compress : {}, + parse : {} }); UglifyJS.base54.reset(); @@ -60,7 +63,8 @@ exports.minify = function(files, options) { sourcesContent[file] = code; toplevel = UglifyJS.parse(code, { filename: options.fromString ? i : file, - toplevel: toplevel + toplevel: toplevel, + bare_returns: options.parse ? options.parse.bare_returns : undefined }); }); } @@ -77,14 +81,21 @@ exports.minify = function(files, options) { toplevel = toplevel.transform(sq); } - // 3. mangle + // 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); } - // 4. output + // 5. output var inMap = options.inSourceMap; var output = {}; if (typeof options.inSourceMap == "string") {