Collapse single use var definitions

Fix #721
This commit is contained in:
kzc
2016-01-27 02:17:06 -05:00
committed by Mihai Bazon
parent 915f907186
commit f4c2ea37bf
3 changed files with 1194 additions and 1 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,
@@ -218,6 +219,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")) {
@@ -226,6 +230,148 @@ 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;
// Scan variable definitions from right to left.
var side_effects_encountered = false;
var lvalues_encountered = false;
var lvalues = {};
for (var var_defs_index = var_defs.length; --var_defs_index >= 0;) {
var var_decl = var_defs[var_defs_index];
if (var_decl.value == null) continue;
// Only interested in cases with just one reference to the variable.
var var_name = var_decl.name.name;
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_IterationStatement
|| (parent instanceof AST_Switch && node !== parent.expression)) {
return 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 = 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);

View File

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

File diff suppressed because it is too large Load Diff