fix pure_funcs & improve side_effects

- only drops side-effect-free arguments
- drop side-effect-free parts with discarded value from `AST_Seq` & `AST_SimpleStatement`

closes #1494
This commit is contained in:
alexlamsl
2017-02-20 01:46:59 +08:00
parent 8898b8a0fe
commit 26fbeece1c
3 changed files with 516 additions and 15 deletions

View File

@@ -83,6 +83,14 @@ function Compressor(options, false_by_default) {
global_defs : {},
passes : 1,
}, true);
var pure_funcs = this.options["pure_funcs"];
if (typeof pure_funcs == "function") {
this.pure_funcs = pure_funcs;
} else {
this.pure_funcs = pure_funcs ? function(node) {
return pure_funcs.indexOf(node.expression.print_to_string()) < 0;
} : return_true;
}
var top_retain = this.options["top_retain"];
if (top_retain instanceof RegExp) {
this.top_retain = function(def) {
@@ -304,6 +312,13 @@ merge(Compressor.prototype, {
}
}
function is_iife_call(node) {
if (node instanceof AST_Call && !(node instanceof AST_New)) {
return node.expression instanceof AST_Function || is_iife_call(node.expression);
}
return false;
}
function tighten_body(statements, compressor) {
var CHANGED, max_iter = 10;
do {
@@ -1354,10 +1369,12 @@ merge(Compressor.prototype, {
def(AST_This, return_false);
def(AST_Call, function(compressor){
var pure = compressor.option("pure_funcs");
if (!pure) return true;
if (typeof pure == "function") return pure(this);
return pure.indexOf(this.expression.print_to_string()) < 0;
if (compressor.pure_funcs(this)) return true;
for (var i = this.args.length; --i >= 0;) {
if (this.args[i].has_side_effects(compressor))
return true;
}
return false;
});
def(AST_Block, function(compressor){
@@ -1855,12 +1872,151 @@ merge(Compressor.prototype, {
return self;
});
// drop_side_effect_free()
// remove side-effect-free parts which only affects return value
(function(def){
function return_this() {
return this;
}
function return_null() {
return null;
}
// Drop side-effect-free elements from an array of expressions.
// Returns an array of expressions with side-effects or null
// if all elements were dropped. Note: original array may be
// returned if nothing changed.
function trim(nodes, compressor, first_in_statement) {
var ret = [], changed = false;
for (var i = 0, ii = nodes.length; i < ii; i++) {
var node = nodes[i].drop_side_effect_free(compressor, first_in_statement);
changed |= node !== nodes[i];
if (node) {
ret.push(node);
first_in_statement = false;
}
}
return changed ? ret.length ? ret : null : nodes;
}
def(AST_Node, return_this);
def(AST_Constant, return_null);
def(AST_This, return_null);
def(AST_Call, function(compressor, first_in_statement){
if (compressor.pure_funcs(this)) return this;
var args = trim(this.args, compressor, first_in_statement);
return args && AST_Seq.from_array(args);
});
def(AST_Function, return_null);
def(AST_Binary, function(compressor, first_in_statement){
var right = this.right.drop_side_effect_free(compressor);
if (!right) return this.left.drop_side_effect_free(compressor, first_in_statement);
switch (this.operator) {
case "&&":
case "||":
var node = this.clone();
node.right = right;
return node;
default:
var left = this.left.drop_side_effect_free(compressor, first_in_statement);
if (!left) return this.right.drop_side_effect_free(compressor, first_in_statement);
return make_node(AST_Seq, this, {
car: left,
cdr: right
});
}
});
def(AST_Assign, return_this);
def(AST_Conditional, function(compressor){
var consequent = this.consequent.drop_side_effect_free(compressor);
var alternative = this.alternative.drop_side_effect_free(compressor);
if (consequent === this.consequent && alternative === this.alternative) return this;
if (!consequent) return alternative ? make_node(AST_Binary, this, {
operator: "||",
left: this.condition,
right: alternative
}) : this.condition.drop_side_effect_free(compressor);
if (!alternative) return make_node(AST_Binary, this, {
operator: "&&",
left: this.condition,
right: consequent
});
var node = this.clone();
node.consequent = consequent;
node.alternative = alternative;
return node;
});
def(AST_Unary, function(compressor, first_in_statement){
switch (this.operator) {
case "delete":
case "++":
case "--":
return this;
case "typeof":
if (this.expression instanceof AST_SymbolRef) return null;
default:
if (first_in_statement && is_iife_call(this.expression)) return this;
return this.expression.drop_side_effect_free(compressor, first_in_statement);
}
});
def(AST_SymbolRef, function() {
return this.undeclared() ? this : null;
});
def(AST_Object, function(compressor, first_in_statement){
var values = trim(this.properties, compressor, first_in_statement);
return values && AST_Seq.from_array(values);
});
def(AST_ObjectProperty, function(compressor, first_in_statement){
return this.value.drop_side_effect_free(compressor, first_in_statement);
});
def(AST_Array, function(compressor, first_in_statement){
var values = trim(this.elements, compressor, first_in_statement);
return values && AST_Seq.from_array(values);
});
def(AST_Dot, function(compressor, first_in_statement){
if (!compressor.option("pure_getters")) return this;
return this.expression.drop_side_effect_free(compressor, first_in_statement);
});
def(AST_Sub, function(compressor, first_in_statement){
if (!compressor.option("pure_getters")) return this;
var expression = this.expression.drop_side_effect_free(compressor, first_in_statement);
if (!expression) return this.property.drop_side_effect_free(compressor, first_in_statement);
var property = this.property.drop_side_effect_free(compressor);
if (!property) return expression;
return make_node(AST_Seq, this, {
car: expression,
cdr: property
});
});
def(AST_Seq, function(compressor){
var cdr = this.cdr.drop_side_effect_free(compressor);
if (cdr === this.cdr) return this;
if (!cdr) return this.car;
return make_node(AST_Seq, this, {
car: this.car,
cdr: cdr
});
});
})(function(node, func){
node.DEFMETHOD("drop_side_effect_free", func);
});
OPT(AST_SimpleStatement, function(self, compressor){
if (compressor.option("side_effects")) {
if (!self.body.has_side_effects(compressor)) {
var body = self.body;
if (!body.has_side_effects(compressor)) {
compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start);
return make_node(AST_EmptyStatement, self);
}
var node = body.drop_side_effect_free(compressor, true);
if (!node) {
compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start);
return make_node(AST_EmptyStatement, self);
}
if (node !== body) {
return make_node(AST_SimpleStatement, self, { body: node });
}
}
return self;
});
@@ -2435,13 +2591,6 @@ merge(Compressor.prototype, {
return self.negate(compressor, true);
}
return self;
function is_iife_call(node) {
if (node instanceof AST_Call && !(node instanceof AST_New)) {
return node.expression instanceof AST_Function || is_iife_call(node.expression);
}
return false;
}
});
OPT(AST_New, function(self, compressor){
@@ -2464,9 +2613,8 @@ merge(Compressor.prototype, {
OPT(AST_Seq, function(self, compressor){
if (!compressor.option("side_effects"))
return self;
if (!self.car.has_side_effects(compressor)) {
return maintain_this_binding(compressor.parent(), self, self.cdr);
}
self.car = self.car.drop_side_effect_free(compressor, first_in_statement(compressor));
if (!self.car) return maintain_this_binding(compressor.parent(), self, self.cdr);
if (compressor.option("cascade")) {
if (self.car instanceof AST_Assign
&& !self.car.left.has_side_effects(compressor)) {

View File

@@ -590,3 +590,61 @@ drop_fnames: {
}
}
}
global_var: {
options = {
side_effects: true,
unused: true,
}
input: {
var a;
function foo(b) {
a;
b;
c;
typeof c === "undefined";
c + b + a;
b && b.ar();
return b;
}
}
expect: {
var a;
function foo(b) {
c;
c;
b && b.ar();
return b;
}
}
}
iife: {
options = {
side_effects: true,
unused: true,
}
input: {
function f() {
var a;
~function() {}(b);
}
}
expect: {
function f() {
~function() {}(b);
}
}
}
drop_value: {
options = {
side_effects: true,
}
input: {
(1, [2, foo()], 3, {a:1, b:bar()});
}
expect: {
foo(), bar();
}
}

295
test/compress/pure_funcs.js Normal file
View File

@@ -0,0 +1,295 @@
array: {
options = {
pure_funcs: [ "Math.floor" ],
side_effects: true,
}
input: {
var a;
function f(b) {
Math.floor(a / b);
Math.floor(c / b);
}
}
expect: {
var a;
function f(b) {
c;
}
}
}
func: {
options = {
pure_funcs: function(node) {
return !~node.args[0].print_to_string().indexOf("a");
},
side_effects: true,
}
input: {
function f(a, b) {
Math.floor(a / b);
Math.floor(c / b);
}
}
expect: {
function f(a, b) {
Math.floor(c / b);
}
}
}
side_effects: {
options = {
pure_funcs: [ "console.log" ],
side_effects: true,
}
input: {
function f(a, b) {
console.log(a());
console.log(b);
}
}
expect: {
function f(a, b) {
a();
}
}
}
unused: {
options = {
pure_funcs: [ "pure" ],
side_effects: true,
unused: true,
}
input: {
function foo() {
var u = pure(1);
var x = pure(2);
var y = pure(x);
var z = pure(pure(side_effects()));
return pure(3);
}
}
expect: {
function foo() {
side_effects();
return pure(3);
}
}
}
babel: {
options = {
pure_funcs: [ "_classCallCheck" ],
side_effects: true,
unused: true,
}
input: {
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor))
throw new TypeError("Cannot call a class as a function");
}
var Foo = function Foo() {
_classCallCheck(this, Foo);
};
}
expect: {
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor))
throw new TypeError("Cannot call a class as a function");
}
var Foo = function() {
};
}
}
conditional: {
options = {
pure_funcs: [ "pure" ],
side_effects: true,
}
input: {
pure(1 | a() ? 2 & b() : 7 ^ c());
pure(1 | a() ? 2 & b() : 5);
pure(1 | a() ? 4 : 7 ^ c());
pure(1 | a() ? 4 : 5);
pure(3 ? 2 & b() : 7 ^ c());
pure(3 ? 2 & b() : 5);
pure(3 ? 4 : 7 ^ c());
pure(3 ? 4 : 5);
}
expect: {
1 | a() ? b() : c();
1 | a() && b();
1 | a() || c();
a();
3 ? b() : c();
3 && b();
3 || c();
}
}
relational: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
foo() in foo();
foo() instanceof bar();
foo() < "bar";
bar() > foo();
bar() != bar();
bar() !== "bar";
"bar" == foo();
"bar" === bar();
"bar" >= "bar";
}
expect: {
bar();
bar();
bar(), bar();
bar();
bar();
}
}
arithmetic: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
foo() + foo();
foo() - bar();
foo() * "bar";
bar() / foo();
bar() & bar();
bar() | "bar";
"bar" >> foo();
"bar" << bar();
"bar" >>> "bar";
}
expect: {
bar();
bar();
bar(), bar();
bar();
bar();
}
}
boolean_and: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
foo() && foo();
foo() && bar();
foo() && "bar";
bar() && foo();
bar() && bar();
bar() && "bar";
"bar" && foo();
"bar" && bar();
"bar" && "bar";
}
expect: {
foo() && bar();
bar();
bar() && bar();
bar();
"bar" && bar();
}
}
boolean_or: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
foo() || foo();
foo() || bar();
foo() || "bar";
bar() || foo();
bar() || bar();
bar() || "bar";
"bar" || foo();
"bar" || bar();
"bar" || "bar";
}
expect: {
foo() || bar();
bar();
bar() || bar();
bar();
"bar" || bar();
}
}
assign: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
var a;
function f(b) {
a = foo();
b *= 4 + foo();
c >>= 0 | foo();
}
}
expect: {
var a;
function f(b) {
a = foo();
b *= 4 + foo();
c >>= 0 | foo();
}
}
}
unary: {
options = {
pure_funcs: [ "foo" ],
side_effects :true,
}
input: {
typeof foo();
typeof bar();
typeof "bar";
void foo();
void bar();
void "bar";
delete a[foo()];
delete a[bar()];
delete a["bar"];
a[foo()]++;
a[bar()]++;
a["bar"]++;
--a[foo()];
--a[bar()];
--a["bar"];
~foo();
~bar();
~"bar";
}
expect: {
bar();
bar();
delete a[foo()];
delete a[bar()];
delete a["bar"];
a[foo()]++;
a[bar()]++;
a["bar"]++;
--a[foo()];
--a[bar()];
--a["bar"];
bar();
}
}