Merge branch 'master' into harmony

This commit is contained in:
Richard van Velzen
2016-04-17 13:37:01 +02:00
36 changed files with 2805 additions and 155 deletions

View File

@@ -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