Compare commits

...

23 Commits

Author SHA1 Message Date
Alex Lam S.L
d13b71297e v3.8.0 2020-02-18 20:32:37 +08:00
Alex Lam S.L
457f958af3 improve --reduce-test (#3727)
- print out Node.js and OS information
2020-02-17 20:56:22 +00:00
Alex Lam S.L
53517db3e4 speed up --reduce-test (#3726)
- avoid pathological test case branches via adaptive time-out
- use initial test case elapsed time to adjust maximum time-out
- index output cache using hash instead of raw source
2020-02-17 15:35:07 +00:00
Alex Lam S.L
c13caf4876 speed up --reduce-test via result caching (#3725) 2020-02-15 22:43:34 +00:00
kzc
fbfa6178a6 improve --reduce-test (#3722)
- hoist body of functions and IIFEs
- simplify var declarations
2020-02-15 20:22:33 +00:00
Alex Lam S.L
5315dd95b0 minor cleanup (#3723) 2020-02-15 17:55:26 +00:00
Marco Gonzalez
31a7bf2a22 Updated "Output options" > "comments" in README.md (#3717)
Expanded the current documentation to include:

- What the value of `"some"` means based on `lib/output.js`.
- Information about the `Function` overload parameters and expected output.
2020-02-15 15:10:58 +00:00
Alex Lam S.L
f0a29902ac enhance properties (#3721) 2020-02-15 13:04:44 +00:00
Alex Lam S.L
0d820e4c0a workaround RegExp formatting bugs (#3720) 2020-02-15 05:26:48 +00:00
Alex Lam S.L
f01f580d6c improve --reduce-test (#3719)
- cover missing cases when eliminating unreferenced labels
- format multi-line outputs correctly
2020-02-14 02:47:20 +00:00
Alex Lam S.L
c01ff76288 improve code reuse (#3718) 2020-02-13 05:16:10 +00:00
Alex Lam S.L
83a42716c3 fix corner case in unused (#3716) 2020-02-12 23:46:16 +00:00
Alex Lam S.L
2557148bba increase mocha --reduce-test timeout (#3715) 2020-02-12 02:25:04 +00:00
Alex Lam S.L
dd22eda888 enhance evaluate (#3714) 2020-02-12 01:01:17 +00:00
Alex Lam S.L
f4c77886e7 add test for --reduce-test (#3712) 2020-02-09 23:21:46 +00:00
Alex Lam S.L
df547ffd97 improve test reduction (#3711)
- scan `AST_SymbolFunarg`
- scan `console.log(...)`
2020-02-09 20:42:36 +00:00
Alex Lam S.L
70551febc8 improve test/reduce (#3710)
- suppress several instances of malformed AST generation
- improve resilience & reporting against malformed ASTs
2020-02-09 08:07:55 +00:00
Alex Lam S.L
44499a6643 fix corner cases in test/reduce (#3709) 2020-02-07 02:41:07 +00:00
Alex Lam S.L
470a7d4df1 improve reduction of AST_BlockStatement (#3708) 2020-02-06 21:20:05 +00:00
Alex Lam S.L
551420132c export missing API for AST manipulation (#3707) 2020-02-06 18:46:25 +00:00
kzc
b0040ba654 implement CLI --reduce-test and reduce tests in ufuzz (#3705) 2020-02-06 02:50:59 +00:00
Alex Lam S.L
c93ca6ee53 fix corner case in ie8 & reduce_vars (#3706)
fixes #3703
2020-02-05 20:03:22 +00:00
Alex Lam S.L
df506439b1 fix corner case in sequences (#3704)
fixes #3703
2020-02-04 04:57:32 +00:00
26 changed files with 1258 additions and 209 deletions

View File

@@ -848,8 +848,14 @@ can pass additional arguments that control the code output:
statement.
- `comments` (default `false`) -- pass `true` or `"all"` to preserve all
comments, `"some"` to preserve some comments, a regular expression string
(e.g. `/^!/`) or a function.
comments, `"some"` to preserve multi-line comments that contain `@cc_on`,
`@license`, or `@preserve` (case-insensitive), a regular expression string
(e.g. `/^!/`), or a function which returns `boolean`, e.g.
```js
function(node, comment) {
return comment.value.indexOf("@type " + node.TYPE) >= 0;
}
```
- `indent_level` (default `4`)

View File

@@ -54,6 +54,7 @@ program.option("--toplevel", "Compress and/or mangle variables in toplevel scope
program.option("--verbose", "Print diagnostic messages.");
program.option("--warn", "Print warning messages.");
program.option("--wrap <name>", "Embed everything as a function with “exports” corresponding to “name” globally.");
program.option("--reduce-test", "Reduce a standalone `console.log` based test case.");
program.arguments("[files...]").parseArgv(process.argv);
if (program.configFile) {
options = JSON.parse(read_file(program.configFile));
@@ -215,7 +216,15 @@ function run() {
} catch (ex) {
fatal(ex);
}
var result = UglifyJS.minify(files, options);
if (program.reduceTest) {
// load on demand - assumes dev tree checked out
var reduce_test = require("../test/reduce");
var testcase = files[0] || files[Object.keys(files)[0]];
var result = reduce_test(testcase, options, {verbose: true});
}
else {
var result = UglifyJS.minify(files, options);
}
if (result.error) {
var ex = result.error;
if (ex.name == "SyntaxError") {

View File

@@ -169,10 +169,7 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", {
}, AST_Statement);
function walk_body(node, visitor) {
var body = node.body;
if (body instanceof AST_Statement) {
body._walk(visitor);
} else body.forEach(function(node) {
node.body.forEach(function(node) {
node._walk(visitor);
});
}
@@ -351,7 +348,7 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", {
filename: "wrap=" + JSON.stringify(name)
}).transform(new TreeTransformer(function(node) {
if (node instanceof AST_Directive && node.value == "$ORIG") {
return MAP.splice(body);
return List.splice(body);
}
}));
},
@@ -370,7 +367,7 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", {
filename: "enclose=" + JSON.stringify(args_values)
}).transform(new TreeTransformer(function(node) {
if (node instanceof AST_Directive && node.value == "$ORIG") {
return MAP.splice(body);
return List.splice(body);
}
}));
}

View File

@@ -1230,7 +1230,7 @@ merge(Compressor.prototype, {
var parent = multi_replacer.parent();
if (parent instanceof AST_Sequence && parent.tail_node() !== node) {
value_def.replaced++;
return MAP.skip;
return List.skip;
}
return get_rvalue(candidate);
case 1:
@@ -1863,7 +1863,7 @@ merge(Compressor.prototype, {
node.value = null;
return node;
}
return in_list ? MAP.skip : null;
return in_list ? List.skip : null;
}, patch_sequence));
}
@@ -3071,12 +3071,17 @@ merge(Compressor.prototype, {
// If the node has been successfully reduced to a constant,
// then its value is returned; otherwise the element itself
// is returned.
//
// They can be distinguished as constant value is never a
// descendant of AST_Node.
AST_Node.DEFMETHOD("evaluate", function(compressor) {
//
// When `ignore_side_effects` is `true`, inspect the constant value
// produced without worrying about any side effects caused by said
// expression.
AST_Node.DEFMETHOD("evaluate", function(compressor, ignore_side_effects) {
if (!compressor.option("evaluate")) return this;
var cached = [];
var val = this._eval(compressor, cached, 1);
var val = this._eval(compressor, ignore_side_effects, cached, 1);
cached.forEach(function(node) {
delete node._eval;
});
@@ -3104,6 +3109,19 @@ merge(Compressor.prototype, {
def(AST_Constant, function() {
return this.value;
});
def(AST_Assign, function(compressor, ignore_side_effects, cached, depth) {
if (!ignore_side_effects) return this;
if (this.operator != "=") return this;
var node = this.right;
var value = node._eval(compressor, ignore_side_effects, cached, depth);
return value === node ? this : value;
});
def(AST_Sequence, function(compressor, ignore_side_effects, cached, depth) {
if (!ignore_side_effects) return this;
var node = this.tail_node();
var value = node._eval(compressor, ignore_side_effects, cached, depth);
return value === node ? this : value;
});
def(AST_Function, function(compressor) {
if (compressor.option("unsafe")) {
var fn = function() {};
@@ -3115,12 +3133,12 @@ merge(Compressor.prototype, {
}
return this;
});
def(AST_Array, function(compressor, cached, depth) {
def(AST_Array, function(compressor, ignore_side_effects, cached, depth) {
if (compressor.option("unsafe")) {
var elements = [];
for (var i = 0; i < this.elements.length; i++) {
var element = this.elements[i];
var value = element._eval(compressor, cached, depth);
var value = element._eval(compressor, ignore_side_effects, cached, depth);
if (element === value) return this;
elements.push(value);
}
@@ -3128,7 +3146,7 @@ merge(Compressor.prototype, {
}
return this;
});
def(AST_Object, function(compressor, cached, depth) {
def(AST_Object, function(compressor, ignore_side_effects, cached, depth) {
if (compressor.option("unsafe")) {
var val = {};
for (var i = 0; i < this.properties.length; i++) {
@@ -3137,14 +3155,14 @@ merge(Compressor.prototype, {
if (key instanceof AST_Symbol) {
key = key.name;
} else if (key instanceof AST_Node) {
key = key._eval(compressor, cached, depth);
key = key._eval(compressor, ignore_side_effects, cached, depth);
if (key === prop.key) return this;
}
if (typeof Object.prototype[key] === 'function') {
return this;
}
if (prop.value instanceof AST_Function) continue;
val[key] = prop.value._eval(compressor, cached, depth);
val[key] = prop.value._eval(compressor, ignore_side_effects, cached, depth);
if (val[key] === prop.value) return this;
}
return val;
@@ -3152,7 +3170,7 @@ merge(Compressor.prototype, {
return this;
});
var non_converting_unary = makePredicate("! typeof void");
def(AST_UnaryPrefix, function(compressor, cached, depth) {
def(AST_UnaryPrefix, function(compressor, ignore_side_effects, cached, depth) {
var e = this.expression;
// Function would be evaluated to an array and so typeof would
// incorrectly return 'object'. Hence making is a special case.
@@ -3164,7 +3182,7 @@ merge(Compressor.prototype, {
return typeof function(){};
}
if (!non_converting_unary[this.operator]) depth++;
var v = e._eval(compressor, cached, depth);
var v = e._eval(compressor, ignore_side_effects, cached, depth);
if (v === this.expression) return this;
switch (this.operator) {
case "!": return !v;
@@ -3187,12 +3205,12 @@ merge(Compressor.prototype, {
return this;
});
var non_converting_binary = makePredicate("&& || === !==");
def(AST_Binary, function(compressor, cached, depth) {
def(AST_Binary, function(compressor, ignore_side_effects, cached, depth) {
if (!non_converting_binary[this.operator]) depth++;
var left = this.left._eval(compressor, cached, depth);
var left = this.left._eval(compressor, ignore_side_effects, cached, depth);
if (left === this.left) return this;
if (this.operator == (left ? "||" : "&&")) return left;
var right = this.right._eval(compressor, cached, depth);
var right = this.right._eval(compressor, ignore_side_effects, cached, depth);
if (right === this.right) return this;
var result;
switch (this.operator) {
@@ -3235,14 +3253,14 @@ merge(Compressor.prototype, {
return (match[1] || ".").length - 1 - (match[2] || "").slice(1);
}
});
def(AST_Conditional, function(compressor, cached, depth) {
var condition = this.condition._eval(compressor, cached, depth);
def(AST_Conditional, function(compressor, ignore_side_effects, cached, depth) {
var condition = this.condition._eval(compressor, ignore_side_effects, cached, depth);
if (condition === this.condition) return this;
var node = condition ? this.consequent : this.alternative;
var value = node._eval(compressor, cached, depth);
var value = node._eval(compressor, ignore_side_effects, cached, depth);
return value === node ? this : value;
});
def(AST_SymbolRef, function(compressor, cached, depth) {
def(AST_SymbolRef, function(compressor, ignore_side_effects, cached, depth) {
var fixed = this.fixed_value();
if (!fixed) return this;
var value;
@@ -3250,7 +3268,7 @@ merge(Compressor.prototype, {
value = fixed._eval();
} else {
this._eval = return_this;
value = fixed._eval(compressor, cached, depth);
value = fixed._eval(compressor, ignore_side_effects, cached, depth);
delete this._eval;
if (value === fixed) return this;
fixed._eval = function() {
@@ -3307,11 +3325,11 @@ merge(Compressor.prototype, {
],
});
var regexp_props = makePredicate("global ignoreCase multiline source");
def(AST_PropAccess, function(compressor, cached, depth) {
def(AST_PropAccess, function(compressor, ignore_side_effects, cached, depth) {
if (compressor.option("unsafe")) {
var key = this.property;
if (key instanceof AST_Node) {
key = key._eval(compressor, cached, depth);
key = key._eval(compressor, ignore_side_effects, cached, depth);
if (key === this.property) return this;
}
var exp = this.expression;
@@ -3321,7 +3339,7 @@ merge(Compressor.prototype, {
if (!static_value || !static_value[key]) return this;
val = global_objs[exp.name];
} else {
val = exp._eval(compressor, cached, depth + 1);
val = exp._eval(compressor, ignore_side_effects, cached, depth + 1);
if (val == null || val === exp) return this;
if (val instanceof RegExp) {
if (!regexp_props[key]) return this;
@@ -3340,7 +3358,7 @@ merge(Compressor.prototype, {
}
return this;
});
def(AST_Call, function(compressor, cached, depth) {
def(AST_Call, function(compressor, ignore_side_effects, cached, depth) {
var exp = this.expression;
var fn = exp instanceof AST_SymbolRef ? exp.fixed_value() : exp;
if (fn instanceof AST_Lambda) {
@@ -3361,14 +3379,14 @@ merge(Compressor.prototype, {
});
});
fn.evaluating = true;
var val = stat.value._eval(compressor, cached, depth);
var val = stat.value._eval(compressor, ignore_side_effects, cached, depth);
delete fn.evaluating;
if (val === stat.value) return this;
return val;
} else if (compressor.option("unsafe") && exp instanceof AST_PropAccess) {
var key = exp.property;
if (key instanceof AST_Node) {
key = key._eval(compressor, cached, depth);
key = key._eval(compressor, ignore_side_effects, cached, depth);
if (key === exp.property) return this;
}
var val;
@@ -3378,7 +3396,7 @@ merge(Compressor.prototype, {
if (!static_fn || !static_fn[key]) return this;
val = global_objs[e.name];
} else {
val = e._eval(compressor, cached, depth + 1);
val = e._eval(compressor, ignore_side_effects, cached, depth + 1);
if (val == null || val === e) return this;
var native_fn = native_fns[val.constructor.name];
if (!native_fn || !native_fn[key]) return this;
@@ -3403,7 +3421,7 @@ merge(Compressor.prototype, {
var values = [];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
var value = arg._eval(compressor, cached, depth);
var value = arg._eval(compressor, ignore_side_effects, cached, depth);
if (arg === value) return;
values.push(value);
}
@@ -3988,6 +4006,8 @@ merge(Compressor.prototype, {
};
// pass 3: we should drop declarations not in_use
var unused_fn_names = [];
var calls_to_drop_args = [];
var fns_with_marked_args = [];
var tt = new TreeTransformer(function(node, descend, in_list) {
var parent = tt.parent();
if (drop_vars) {
@@ -4017,7 +4037,7 @@ merge(Compressor.prototype, {
if (value) props.push(value);
switch (props.length) {
case 0:
return MAP.skip;
return List.skip;
case 1:
return maintain_this_binding(compressor, parent, node, props[0].transform(tt));
default:
@@ -4028,6 +4048,7 @@ merge(Compressor.prototype, {
}
}
}
if (node instanceof AST_Call) calls_to_drop_args.push(node);
if (scope !== self) return;
if (node instanceof AST_Function && node.name && drop_fn_name(node.name.definition())) {
unused_fn_names.push(node);
@@ -4046,6 +4067,7 @@ merge(Compressor.prototype, {
trim = false;
}
}
fns_with_marked_args.push(node);
}
if (drop_funcs && node instanceof AST_Defun && node !== self) {
var def = node.name.definition();
@@ -4159,11 +4181,11 @@ merge(Compressor.prototype, {
}
switch (body.length) {
case 0:
return in_list ? MAP.skip : make_node(AST_EmptyStatement, node);
return in_list ? List.skip : make_node(AST_EmptyStatement, node);
case 1:
return body[0];
default:
return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, {
return in_list ? List.splice(body) : make_node(AST_BlockStatement, node, {
body: body
});
}
@@ -4177,7 +4199,7 @@ merge(Compressor.prototype, {
var block = node.body;
node.body = block.body.pop();
block.body.push(node);
return in_list ? MAP.splice(block.body) : block;
return in_list ? List.splice(block.body) : block;
}
return node;
}
@@ -4214,7 +4236,7 @@ merge(Compressor.prototype, {
} else if (is_empty(node.init)) {
node.init = null;
}
return !block ? node : in_list ? MAP.splice(block.body) : block;
return !block ? node : in_list ? List.splice(block.body) : block;
} else if (node instanceof AST_ForIn) {
if (!drop_vars || !compressor.option("loops")) return;
if (!(node.init instanceof AST_Definitions)) return;
@@ -4229,7 +4251,7 @@ merge(Compressor.prototype, {
body: value
});
}
return in_list ? MAP.skip : make_node(AST_EmptyStatement, node);
return in_list ? List.skip : make_node(AST_EmptyStatement, node);
} else if (node instanceof AST_Sequence) {
if (node.expressions.length == 1) return node.expressions[0];
}
@@ -4239,6 +4261,9 @@ merge(Compressor.prototype, {
unused_fn_names.forEach(function(fn) {
fn.name = null;
});
calls_to_drop_args.forEach(function(call) {
drop_unused_call_args(call, compressor, fns_with_marked_args);
});
function log(sym, text, props) {
AST_Node[sym.unreferenced() ? "warn" : "info"](text, props);
@@ -4478,7 +4503,7 @@ merge(Compressor.prototype, {
node.in_bool = true;
var value = node.value;
if (value) {
var ev = value.is_truthy() || value.tail_node().evaluate(compressor);
var ev = value.is_truthy() || value.evaluate(compressor, true);
if (!ev) {
value = value.drop_side_effect_free(compressor);
node.value = value ? make_sequence(node.value, [
@@ -4610,7 +4635,7 @@ merge(Compressor.prototype, {
}));
});
defs_by_id[node.name.definition().id] = defs;
return MAP.splice(var_defs);
return List.splice(var_defs);
}
function make_sym(sym, key) {
@@ -4746,21 +4771,17 @@ merge(Compressor.prototype, {
return exprs && make_sequence(this, exprs);
}
if (exp instanceof AST_Function && (!exp.name || !exp.name.definition().references.length)) {
var node = this.clone();
exp.process_expression(false, function(node) {
var value = node.value && node.value.drop_side_effect_free(compressor, true);
return value ? make_node(AST_SimpleStatement, node, {
body: value
}) : make_node(AST_EmptyStatement, node);
});
exp.walk(new TreeWalker(function(node) {
if (node instanceof AST_Return && node.value) {
node.value = node.value.drop_side_effect_free(compressor);
return true;
}
if (node instanceof AST_Scope && node !== exp) return true;
}));
return node;
scan_local_returns(exp, function(node) {
if (node.value) node.value = node.value.drop_side_effect_free(compressor);
});
// always shallow clone to ensure stripping of negated IIFEs
return this.clone();
}
return this;
}
@@ -4878,7 +4899,7 @@ merge(Compressor.prototype, {
OPT(AST_Do, function(self, compressor) {
if (!compressor.option("loops")) return self;
var cond = self.condition.is_truthy() || self.condition.tail_node().evaluate(compressor);
var cond = self.condition.is_truthy() || self.condition.evaluate(compressor, true);
if (!(cond instanceof AST_Node)) {
if (cond) return make_node(AST_For, self, {
body: make_node(AST_BlockStatement, self.body, {
@@ -5008,16 +5029,14 @@ merge(Compressor.prototype, {
}
if (self.condition) {
var cond = self.condition.evaluate(compressor);
if (!(cond instanceof AST_Node)) {
if (cond) self.condition = null;
else if (!compressor.option("dead_code")) {
var orig = self.condition;
self.condition = make_node_from_constant(cond, self.condition);
self.condition = best_of_expression(self.condition.transform(compressor), orig);
}
}
if (cond instanceof AST_Node) {
cond = self.condition.is_truthy() || self.condition.tail_node().evaluate(compressor);
cond = self.condition.is_truthy() || self.condition.evaluate(compressor, true);
} else if (cond) {
self.condition = null;
} else if (!compressor.option("dead_code")) {
var orig = self.condition;
self.condition = make_node_from_constant(cond, self.condition);
self.condition = best_of_expression(self.condition.transform(compressor), orig);
}
if (!cond) {
if (compressor.option("dead_code")) {
@@ -5109,7 +5128,7 @@ merge(Compressor.prototype, {
}
if (compressor.option("dead_code")) {
if (cond instanceof AST_Node) {
cond = self.condition.is_truthy() || self.condition.tail_node().evaluate(compressor);
cond = self.condition.is_truthy() || self.condition.evaluate(compressor, true);
}
if (!cond) {
AST_Node.warn("Condition always false [{file}:{line},{col}]", self.condition.start);
@@ -5255,7 +5274,7 @@ merge(Compressor.prototype, {
}
if (!compressor.option("dead_code")) return self;
if (value instanceof AST_Node) {
value = self.expression.tail_node().evaluate(compressor);
value = self.expression.evaluate(compressor, true);
}
var decl = [];
var body = [];
@@ -5277,7 +5296,7 @@ merge(Compressor.prototype, {
eliminate_branch(branch, body[body.length - 1]);
continue;
}
if (exp instanceof AST_Node) exp = branch.expression.tail_node().evaluate(compressor);
if (exp instanceof AST_Node) exp = branch.expression.evaluate(compressor, true);
if (exp === value) {
exact_match = branch;
if (default_branch) {
@@ -5437,6 +5456,62 @@ merge(Compressor.prototype, {
return make_sequence(node, x);
}
function drop_unused_call_args(call, compressor, fns_with_marked_args) {
var exp = call.expression;
var fn = exp instanceof AST_SymbolRef ? exp.fixed_value() : exp;
if (!(fn instanceof AST_Lambda)) return;
if (fn.uses_arguments) return;
if (fn.pinned()) return;
if (fns_with_marked_args && fns_with_marked_args.indexOf(fn) < 0) return;
var args = call.args;
var pos = 0, last = 0;
var drop_fargs = fn === exp && !fn.name && compressor.drop_fargs(fn, call);
var side_effects = [];
for (var i = 0; i < args.length; i++) {
var trim = i >= fn.argnames.length;
if (trim || fn.argnames[i].__unused) {
var node = args[i].drop_side_effect_free(compressor);
if (drop_fargs) {
fn.argnames.splice(i, 1);
args.splice(i, 1);
if (node) side_effects.push(node);
i--;
continue;
} else if (node) {
side_effects.push(node);
args[pos++] = make_sequence(call, side_effects);
side_effects = [];
} else if (!trim) {
if (side_effects.length) {
node = make_sequence(call, side_effects);
side_effects = [];
} else {
node = make_node(AST_Number, args[i], {
value: 0
});
}
args[pos++] = node;
continue;
}
} else {
side_effects.push(args[i]);
args[pos++] = make_sequence(call, side_effects);
side_effects = [];
}
last = pos;
}
if (drop_fargs) for (; i < fn.argnames.length; i++) {
if (fn.argnames[i].__unused) fn.argnames.splice(i--, 1);
}
args.length = last;
if (!side_effects.length) return;
var arg = make_sequence(call, side_effects);
args.push(args.length < fn.argnames.length ? make_node(AST_UnaryPrefix, call, {
operator: "void",
expression: arg
}) : arg);
}
OPT(AST_Call, function(self, compressor) {
var exp = self.expression;
if (compressor.option("sequences")) {
@@ -5453,63 +5528,7 @@ merge(Compressor.prototype, {
if (seq !== self) return seq.optimize(compressor);
}
}
var fn = exp;
if (compressor.option("reduce_vars") && fn instanceof AST_SymbolRef) {
fn = fn.fixed_value();
}
var is_func = fn instanceof AST_Lambda;
if (compressor.option("unused")
&& is_func
&& !fn.uses_arguments
&& !fn.pinned()) {
var pos = 0, last = 0;
var drop_fargs = exp === fn && !fn.name && compressor.drop_fargs(fn, self);
var side_effects = [];
for (var i = 0; i < self.args.length; i++) {
var trim = i >= fn.argnames.length;
if (trim || fn.argnames[i].__unused) {
var node = self.args[i].drop_side_effect_free(compressor);
if (drop_fargs) {
fn.argnames.splice(i, 1);
self.args.splice(i, 1);
if (node) side_effects.push(node);
i--;
continue;
} else if (node) {
side_effects.push(node);
self.args[pos++] = make_sequence(self, side_effects);
side_effects = [];
} else if (!trim) {
if (side_effects.length) {
node = make_sequence(self, side_effects);
side_effects = [];
} else {
node = make_node(AST_Number, self.args[i], {
value: 0
});
}
self.args[pos++] = node;
continue;
}
} else {
side_effects.push(self.args[i]);
self.args[pos++] = make_sequence(self, side_effects);
side_effects = [];
}
last = pos;
}
if (drop_fargs) for (; i < fn.argnames.length; i++) {
if (fn.argnames[i].__unused) fn.argnames.splice(i--, 1);
}
self.args.length = last;
if (side_effects.length) {
var arg = make_sequence(self, side_effects);
self.args.push(self.args.length < fn.argnames.length ? make_node(AST_UnaryPrefix, self, {
operator: "void",
expression: arg
}) : arg);
}
}
if (compressor.option("unused")) drop_unused_call_args(self, compressor);
if (compressor.option("unsafe")) {
if (is_undeclared_ref(exp)) switch (exp.name) {
case "Array":
@@ -5775,6 +5794,8 @@ merge(Compressor.prototype, {
}
}
}
var fn = exp instanceof AST_SymbolRef ? exp.fixed_value() : exp;
var is_func = fn instanceof AST_Lambda;
var stat = is_func && fn.first_statement();
var can_inline = compressor.option("inline") && !self.is_expr_pure(compressor);
if (exp === fn && can_inline && stat instanceof AST_Return) {
@@ -6162,34 +6183,43 @@ merge(Compressor.prototype, {
});
AST_Binary.DEFMETHOD("lift_sequences", function(compressor) {
if (compressor.option("sequences")) {
if (this.left instanceof AST_Sequence) {
var x = this.left.expressions.slice();
var e = this.clone();
e.left = x.pop();
x.push(e);
return make_sequence(this, x).optimize(compressor);
if (this.left instanceof AST_PropAccess) {
if (!(this.left.expression instanceof AST_Sequence)) return this;
var x = this.left.expression.expressions.slice();
var e = this.clone();
e.left = e.left.clone();
e.left.expression = x.pop();
x.push(e);
return make_sequence(this, x);
}
if (this.left instanceof AST_Sequence) {
var x = this.left.expressions.slice();
var e = this.clone();
e.left = x.pop();
x.push(e);
return make_sequence(this, x);
}
if (this.right instanceof AST_Sequence) {
if (this.left.has_side_effects(compressor)) return this;
var assign = this.operator == "=" && this.left instanceof AST_SymbolRef;
var x = this.right.expressions;
var last = x.length - 1;
for (var i = 0; i < last; i++) {
if (!assign && x[i].has_side_effects(compressor)) break;
}
if (this.right instanceof AST_Sequence && !this.left.has_side_effects(compressor)) {
var assign = this.operator == "=" && this.left instanceof AST_SymbolRef;
var x = this.right.expressions;
var last = x.length - 1;
for (var i = 0; i < last; i++) {
if (!assign && x[i].has_side_effects(compressor)) break;
}
if (i == last) {
x = x.slice();
var e = this.clone();
e.right = x.pop();
x.push(e);
return make_sequence(this, x).optimize(compressor);
} else if (i > 0) {
var e = this.clone();
e.right = make_sequence(this.right, x.slice(i));
x = x.slice(0, i);
x.push(e);
return make_sequence(this, x).optimize(compressor);
}
if (i == last) {
x = x.slice();
var e = this.clone();
e.right = x.pop();
x.push(e);
return make_sequence(this, x);
}
if (i > 0) {
var e = this.clone();
e.right = make_sequence(this.right, x.slice(i));
x = x.slice(0, i);
x.push(e);
return make_sequence(this, x);
}
}
return this;
@@ -6240,8 +6270,10 @@ merge(Compressor.prototype, {
// result. hence, force switch.
reverse();
}
var seq = self.lift_sequences(compressor);
if (seq !== self) return seq;
if (compressor.option("sequences")) {
var seq = self.lift_sequences(compressor);
if (seq !== self) return seq.optimize(compressor);
}
if (compressor.option("assignments") && lazy_op[self.operator]) {
var assign = self.right;
// a || (a = x) => a = a || x
@@ -6450,7 +6482,7 @@ merge(Compressor.prototype, {
}
// (x || false) && y => x ? y : false
if (self.left.operator == "||") {
var lr = self.left.right.tail_node().evaluate(compressor);
var lr = self.left.right.evaluate(compressor, true);
if (!lr) return make_node(AST_Conditional, self, {
condition: self.left.left,
consequent: self.right,
@@ -6484,7 +6516,7 @@ merge(Compressor.prototype, {
}
// x && true || y => x ? true : y
if (self.left.operator == "&&") {
var lr = self.left.right.is_truthy() || self.left.right.tail_node().evaluate(compressor);
var lr = self.left.right.is_truthy() || self.left.right.evaluate(compressor, true);
if (lr && !(lr instanceof AST_Node)) return make_node(AST_Conditional, self, {
condition: self.left.left,
consequent: self.left.right,
@@ -6953,9 +6985,9 @@ merge(Compressor.prototype, {
var fn = node.fixed_value();
if (!(fn instanceof AST_Lambda)) return;
if (!fn.name) return;
var fn_def = fn.name.definition();
if (fn_def.scope !== fn.name.scope) return;
if (fixed.variables.get(fn.name.name) !== fn_def) return;
if (fn.name.definition() !== def) return;
if (def.scope !== fn.name.scope) return;
if (fixed.variables.get(fn.name.name) !== def) return;
fn.name = fn.name.clone();
var value_def = value.variables.get(fn.name.name) || value.def_function(fn.name);
node.thedef = value_def;
@@ -7158,8 +7190,10 @@ merge(Compressor.prototype, {
|| parent instanceof AST_UnaryPrefix);
}
}
var seq = self.lift_sequences(compressor);
if (seq !== self) return seq;
if (compressor.option("sequences")) {
var seq = self.lift_sequences(compressor);
if (seq !== self) return seq.optimize(compressor);
}
if (!compressor.option("assignments")) return self;
if (self.operator == "=" && self.left instanceof AST_SymbolRef && self.right instanceof AST_Binary) {
// x = expr1 OP expr2
@@ -7581,16 +7615,14 @@ merge(Compressor.prototype, {
value = value.fixed_value();
}
if (!value) return false;
return !(value instanceof AST_Lambda)
|| compressor.parent() instanceof AST_New
|| !value.contains_this();
if (!(value instanceof AST_Lambda)) return true;
var parent = compressor.parent();
if (parent.TYPE != "Call") return true;
if (parent.expression !== compressor.self()) return true;
return !value.contains_this();
}
OPT(AST_Sub, function(self, compressor) {
if (compressor.option("sequences") && compressor.parent().TYPE != "Call") {
var seq = lift_sequence_in_expression(self, compressor);
if (seq !== self) return seq.optimize(compressor);
}
var expr = self.expression;
var prop = self.property;
if (compressor.option("properties")) {
@@ -7661,6 +7693,10 @@ merge(Compressor.prototype, {
}
}
if (is_lhs(compressor.self(), parent)) return self;
if (compressor.option("sequences") && compressor.parent().TYPE != "Call") {
var seq = lift_sequence_in_expression(self, compressor);
if (seq !== self) return seq.optimize(compressor);
}
if (key !== prop) {
var sub = self.flatten_object(property, compressor);
if (sub) {
@@ -7755,10 +7791,6 @@ merge(Compressor.prototype, {
});
OPT(AST_Dot, function(self, compressor) {
if (compressor.option("sequences") && compressor.parent().TYPE != "Call") {
var seq = lift_sequence_in_expression(self, compressor);
if (seq !== self) return seq.optimize(compressor);
}
if (self.property == "arguments" || self.property == "caller") {
AST_Node.warn("Function.prototype.{prop} not supported [{file}:{line},{col}]", {
prop: self.property,
@@ -7768,6 +7800,10 @@ merge(Compressor.prototype, {
});
}
if (is_lhs(compressor.self(), compressor.parent())) return self;
if (compressor.option("sequences") && compressor.parent().TYPE != "Call") {
var seq = lift_sequence_in_expression(self, compressor);
if (seq !== self) return seq.optimize(compressor);
}
if (compressor.option("unsafe_proto")
&& self.expression instanceof AST_Dot
&& self.expression.property == "prototype") {

View File

@@ -783,6 +783,8 @@ function OutputStream(options) {
var p = output.parent();
if (p instanceof AST_PropAccess && p.expression === this) {
var value = this.value;
// https://github.com/mishoo/UglifyJS2/issues/115
// https://github.com/mishoo/UglifyJS2/pull/1009
if (value < 0 || /^0/.test(make_num(value))) {
return true;
}
@@ -1352,11 +1354,18 @@ function OutputStream(options) {
DEFPRINT(AST_RegExp, function(self, output) {
var regexp = self.value;
var str = regexp.toString();
var end = str.lastIndexOf("/");
if (regexp.raw_source) {
str = "/" + regexp.raw_source + str.slice(str.lastIndexOf("/"));
str = "/" + regexp.raw_source + str.slice(end);
} else if (end == 1) {
str = "/(?:)" + str.slice(end);
} else if (str.indexOf("/", 1) < end) {
str = "/" + str.slice(1, end).replace(/\\\\|[^/]?\//g, function(match) {
return match[0] == "\\" ? match : match.slice(0, -1) + "\\/";
}) + str.slice(end);
}
output.print(output.to_utf8(str).replace(/\\(?:\0(?![0-9])|[^\0])/g, function(seq) {
switch (seq[1]) {
output.print(output.to_utf8(str).replace(/\\(?:\0(?![0-9])|[^\0])/g, function(match) {
switch (match[1]) {
case "\n": return "\\n";
case "\r": return "\\r";
case "\t": return "\t";
@@ -1366,7 +1375,7 @@ function OutputStream(options) {
case "\x0B": return "\v";
case "\u2028": return "\\u2028";
case "\u2029": return "\\u2029";
default: return seq;
default: return match;
}
}).replace(/[\n\r\u2028\u2029]/g, function(c) {
switch (c) {

View File

@@ -52,7 +52,7 @@ TreeTransformer.prototype = new TreeWalker;
(function(DEF) {
function do_list(list, tw) {
return MAP(list, function(node) {
return List(list, function(node) {
return node.transform(tw, true);
});
}

View File

@@ -113,8 +113,8 @@ function return_true() { return true; }
function return_this() { return this; }
function return_null() { return null; }
var MAP = (function() {
function MAP(a, f, backwards) {
var List = (function() {
function List(a, f, backwards) {
var ret = [], top = [], i;
function doit() {
var val = f(a[i], i);
@@ -149,14 +149,14 @@ var MAP = (function() {
}
return top.concat(ret);
}
MAP.at_top = function(val) { return new AtTop(val) };
MAP.splice = function(val) { return new Splice(val) };
MAP.last = function(val) { return new Last(val) };
var skip = MAP.skip = {};
function AtTop(val) { this.v = val }
function Splice(val) { this.v = val }
function Last(val) { this.v = val }
return MAP;
List.at_top = function(val) { return new AtTop(val); };
List.splice = function(val) { return new Splice(val); };
List.last = function(val) { return new Last(val); };
var skip = List.skip = {};
function AtTop(val) { this.v = val; }
function Splice(val) { this.v = val; }
function Last(val) { this.v = val; }
return List;
})();
function push_uniq(array, el) {

View File

@@ -3,7 +3,7 @@
"description": "JavaScript parser, mangler/compressor and beautifier toolkit",
"author": "Mihai Bazon <mihai.bazon@gmail.com> (http://lisperator.net/)",
"license": "BSD-2-Clause",
"version": "3.7.7",
"version": "3.8.0",
"engines": {
"node": ">=0.8.0"
},

View File

@@ -1191,10 +1191,10 @@ issue_2105_1: {
input: {
!function(factory) {
factory();
}( function() {
}(function() {
return function(fn) {
fn()().prop();
}( function() {
}(function() {
function bar() {
var quux = function() {
console.log("PASS");
@@ -1205,7 +1205,7 @@ issue_2105_1: {
return { prop: foo };
}
return bar;
} );
});
});
}
expect: {
@@ -1235,10 +1235,10 @@ issue_2105_2: {
input: {
!function(factory) {
factory();
}( function() {
}(function() {
return function(fn) {
fn()().prop();
}( function() {
}(function() {
function bar() {
var quux = function() {
console.log("PASS");
@@ -1249,7 +1249,7 @@ issue_2105_2: {
return { prop: foo };
}
return bar;
} );
});
});
}
expect: {
@@ -1258,6 +1258,44 @@ issue_2105_2: {
expect_stdout: "PASS"
}
issue_2105_3: {
options = {
inline: true,
passes: 2,
reduce_vars: true,
unused: true,
}
input: {
!function(factory) {
factory();
}(function() {
return function(fn) {
fn()().prop();
}(function() {
function bar() {
var quux = function() {
console.log("PASS");
}, foo = function() {
console.log;
quux();
};
return { prop: foo };
}
return bar;
});
});
}
expect: {
!void void {
prop: function() {
console.log;
void console.log("PASS");
}
}.prop();
}
expect_stdout: "PASS"
}
issue_2226_1: {
options = {
side_effects: true,
@@ -2330,7 +2368,7 @@ function_parameter_ie8: {
(function() {
(function f() {
console.log("PASS");
})(0);
})();
})();
}
expect_stdout: "PASS"

View File

@@ -1276,7 +1276,7 @@ issue_2630_3: {
(function() {
(function f1(a) {
f2();
--x >= 0 && f1({});
--x >= 0 && f1();
})(a++);
function f2() {
a++;

View File

@@ -2361,3 +2361,40 @@ issue_3542: {
}
expect_stdout: "1"
}
issue_3703: {
options = {
ie8: true,
reduce_vars: true,
toplevel: true,
unused: true,
}
input: {
var a = "PASS";
function f() {
var b;
function g() {
a = "FAIL";
}
var c = g;
function h() {
f;
}
a ? b |= c : b.p;
}
f();
console.log(a);
}
expect: {
var a = "PASS";
(function() {
var b;
var c = function g() {
a = "FAIL";
};
a ? b |= c : b.p;
})();
console.log(a);
}
expect_stdout: "PASS"
}

View File

@@ -728,7 +728,7 @@ issue_2630_3: {
(function() {
(function f1() {
f2();
--x >= 0 && f1({});
--x >= 0 && f1();
})(a++);
function f2() {
a++;
@@ -1369,7 +1369,7 @@ recursive_iife_1: {
}
expect: {
console.log(function f(a, b) {
return b || f("FAIL", "PASS");
return b || f(0, "PASS");
}());
}
expect_stdout: "PASS"
@@ -1388,7 +1388,7 @@ recursive_iife_2: {
}
expect: {
console.log(function f(a, b) {
return b || f("FAIL", "PASS");
return b || f(0, "PASS");
}(0, 0));
}
expect_stdout: "PASS"
@@ -1416,7 +1416,7 @@ recursive_iife_3: {
var a = 1, c = "PASS";
(function() {
(function f(b, d, e) {
a-- && f(null, 42, 0);
a-- && f(0, 42, 0);
e && (c = "FAIL");
d && d.p;
})();

View File

@@ -817,6 +817,29 @@ issue_2208_5: {
expect_stdout: "42"
}
issue_2208_6: {
options = {
inline: true,
properties: true,
side_effects: true,
}
input: {
a = 42;
console.log(("FAIL", {
p: function() {
return this.a;
}
}.p)());
}
expect: {
a = 42;
console.log(function() {
return this.a;
}());
}
expect_stdout: "42"
}
issue_2256: {
options = {
side_effects: true,

View File

@@ -2071,13 +2071,8 @@ issue_1670_6: {
}
expect: {
(function(a) {
switch (1) {
case a = 1:
console.log(a);
break;
default:
console.log(2);
}
a = 1;
console.log(a);
})(1);
}
expect_stdout: "1"

View File

@@ -186,3 +186,72 @@ issue_3434_3: {
/\nfo\n[\n]o\bbb/;
}
}
issue_3434_4: {
options = {
evaluate: true,
unsafe: true,
}
input: {
[
[ "", RegExp("") ],
[ "/", RegExp("/") ],
[ "//", RegExp("//") ],
[ "\/", RegExp("\\/") ],
[ "///", RegExp("///") ],
[ "/\/", RegExp("/\\/") ],
[ "\//", RegExp("\\//") ],
[ "\\/", RegExp("\\\\/") ],
[ "////", RegExp("////") ],
[ "//\/", RegExp("//\\/") ],
[ "/\//", RegExp("/\\//") ],
[ "/\\/", RegExp("/\\\\/") ],
[ "\///", RegExp("\\///") ],
[ "\/\/", RegExp("\\/\\/") ],
[ "\\//", RegExp("\\\\//") ],
[ "\\\/", RegExp("\\\\\\/") ],
].forEach(function(test) {
console.log(test[1].test("\\"), test[1].test(test[0]));
});
}
expect: {
[
[ "", /(?:)/ ],
[ "/", /\// ],
[ "//", /\/\// ],
[ "/", /\// ],
[ "///", /\/\/\// ],
[ "//", /\/\// ],
[ "//", /\/\// ],
[ "\\/", /\\\// ],
[ "////", /\/\/\/\// ],
[ "///", /\/\/\// ],
[ "///", /\/\/\// ],
[ "/\\/", /\/\\\// ],
[ "///", /\/\/\// ],
[ "//", /\/\// ],
[ "\\//", /\\\/\// ],
[ "\\/", /\\\// ],
].forEach(function(test) {
console.log(test[1].test("\\"), test[1].test(test[0]));
});
}
expect_stdout: [
"true true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
"false true",
]
}

View File

@@ -1093,3 +1093,22 @@ issue_3490_2: {
}
expect_stdout: "PASS 42"
}
issue_3703: {
options = {
evaluate: true,
sequences: true,
unsafe: true,
}
input: {
var a = "FAIL";
while ((a = "PASS", 0).foo = 0);
console.log(a);
}
expect: {
var a = "FAIL";
while (a = "PASS", (0).foo = 0);
console.log(a);
}
expect_stdout: "PASS"
}

View File

@@ -1,6 +1,7 @@
exports["Compressor"] = Compressor;
exports["defaults"] = defaults;
exports["JS_Parse_Error"] = JS_Parse_Error;
exports["List"] = List;
exports["mangle_properties"] = mangle_properties;
exports["minify"] = minify;
exports["OutputStream"] = OutputStream;

View File

@@ -0,0 +1,9 @@
var o = this;
for (var k in o) L17060: {
a++;
}
var a;
console.log(k);

View File

@@ -0,0 +1,16 @@
var o = this;
for (var k in o) {
0;
}
var a;
console.log(k);
// output: a
//
// minify: k
//
// options: {
// "mangle": false
// }

View File

@@ -0,0 +1,18 @@
var _calls_ = 10, a = 100, b = 10, c = 0;
function f0(b_1, a, undefined_2) {
a++ + ++b;
{
var expr2 = (b + 1 - .1 - .1 - .1 || a || 3).toString();
L20778: for (var key2 in expr2) {
(c = c + 1) + [ --b + b_1, typeof f0 == "function" && --_calls_ >= 0 && f0(--b + typeof (undefined_2 = 1 === 1 ? a : b), --b + {
c: (c = c + 1) + null
}, a++ + (typeof f0 == "function" && --_calls_ >= 0 && f0(typeof (c = 1 + c, 3 / "a" * ("c" >>> 23..toString()) >= (b_1 && (b_1[(c = c + 1) + a--] = (- -0,
true + {})))), 3, 25))), 1 === 1 ? a : b ];
}
}
}
var a_1 = f0([ , 0 ].length === 2);
console.log(null, a, b, c, Infinity, NaN, undefined);

View File

@@ -0,0 +1,19 @@
var b = 0;
var expr2 = (0 - 1 - .1 - .1).toString();
for (var key2 in expr2) {
--b;
}
console.log(b);
// output: -19
//
// minify: -4
//
// options: {
// "compress": {
// "unsafe_math": true
// },
// "mangle": false
// }

167
test/mocha/reduce.js Normal file
View File

@@ -0,0 +1,167 @@
var assert = require("assert");
var exec = require("child_process").exec;
var fs = require("fs");
var reduce_test = require("../reduce");
var semver = require("semver");
function read(path) {
return fs.readFileSync(path, "utf8");
}
describe("test/reduce.js", function() {
this.timeout(60000);
it("Should reduce test case", function() {
var result = reduce_test(read("test/input/reduce/unsafe_math.js"), {
compress: {
unsafe_math: true,
},
mangle: false,
}, {
verbose: false,
});
if (result.error) throw result.error;
assert.strictEqual(result.code, read("test/input/reduce/unsafe_math.reduced.js"));
});
it("Should eliminate unreferenced labels", function() {
var result = reduce_test(read("test/input/reduce/label.js"), {
mangle: false,
}, {
verbose: false,
});
if (result.error) throw result.error;
assert.strictEqual(result.code, read("test/input/reduce/label.reduced.js"));
});
it("Should handle test cases with --toplevel", function() {
var result = reduce_test([
"var Infinity = 42;",
"console.log(Infinity);",
].join("\n"), {
toplevel: true,
});
if (result.error) throw result.error;
assert.strictEqual(result.code, [
"// Can't reproduce test failure with minify options provided:",
"// {",
'// "toplevel": true',
"// }",
].join("\n"));
});
it("Should handle test result of NaN", function() {
var result = reduce_test("throw 0 / 0;");
if (result.error) throw result.error;
assert.strictEqual(result.code, [
"// Can't reproduce test failure with minify options provided:",
"// {",
'// "compress": {},',
'// "mangle": false',
"// }",
].join("\n"));
});
it("Should print correct output for irreducible test case", function() {
var result = reduce_test([
"console.log(function f(a) {",
" return f.length;",
"}());",
].join("\n"), {
compress: {
keep_fargs: false,
},
mangle: false,
});
if (result.error) throw result.error;
assert.strictEqual(result.code, [
"console.log(function f(a) {",
" return f.length;",
"}());",
"// output: 1",
"// ",
"// minify: 0",
"// ",
"// options: {",
'// "compress": {',
'// "keep_fargs": false',
"// },",
'// "mangle": false',
"// }",
].join("\n"));
});
it("Should fail when invalid option is supplied", function() {
var result = reduce_test("", {
compress: {
unsafe_regex: true,
},
});
var err = result.error;
assert.ok(err instanceof Error);
assert.strictEqual(err.stack.split(/\n/)[0], "DefaultsError: `unsafe_regex` is not a supported option");
});
it("Should report on test case with invalid syntax", function() {
var result = reduce_test("var 0 = 1;");
var err = result.error;
assert.ok(err instanceof Error);
assert.strictEqual(err.stack.split(/\n/)[0], "SyntaxError: Name expected");
});
it("Should format multi-line output correctly", function() {
var code = [
"var a = 0;",
"",
"for (var b in [ 1, 2, 3 ]) {",
" a = +a + 1 - .2;",
" console.log(a);",
"}",
].join("\n");
var result = reduce_test(code, {
compress: {
unsafe_math: true,
},
mangle: false,
});
if (result.error) throw result.error;
assert.strictEqual(result.code, [
code,
"// output: 0.8",
"// 1.6",
"// 2.4",
"// ",
"// minify: 0.8",
"// 1.6",
"// 2.4000000000000004",
"// ",
"// options: {",
'// "compress": {',
'// "unsafe_math": true',
"// },",
'// "mangle": false',
"// }",
].join("\n"));
});
it("Should reduce infinite loops with reasonable performance", function() {
if (semver.satisfies(process.version, "0.10")) return;
this.timeout(120000);
var code = [
"var a = 9007199254740992, b = 1;",
"",
"while (a++ + (1 - b) < a) {",
" 0;",
"}",
].join("\n");
var result = reduce_test(code, {
compress: {
unsafe_math: true,
},
mangle: false,
});
if (result.error) throw result.error;
assert.strictEqual(result.code.replace(/ timed out after [0-9]+ms/, " timed out."), [
code,
"// output: ",
"// minify: Error: Script execution timed out.",
"// options: {",
'// "compress": {',
'// "unsafe_math": true',
"// },",
'// "mangle": false',
"// }",
].join("\n"));
});
});

564
test/reduce.js Normal file
View File

@@ -0,0 +1,564 @@
var crypto = require("crypto");
var U = require("./node");
var List = U.List;
var os = require("os");
var sandbox = require("./sandbox");
// Reduce a ufuzz-style `console.log` based test case by iteratively replacing
// AST nodes with various permutations. Each AST_Statement in the tree is also
// speculatively dropped to determine whether it is needed. If the altered
// tree and the last known good tree produce the same non-nil error-free output
// after being run, then the permutation survives to the next generation and
// is the basis for subsequent iterations. The test case is reduced as a
// consequence of complex expressions being replaced with simpler ones.
// This function assumes that the testcase will not result in a parse or
// runtime Error. Note that a reduced test case will have different runtime
// output - it is not functionally equivalent to the original. The only criteria
// is that once the generated reduced test case is run without minification, it
// will produce different output from the code minified with `minify_options`.
// Returns a `minify` result object with an additonal boolean property `reduced`.
module.exports = function reduce_test(testcase, minify_options, reduce_options) {
if (testcase instanceof U.AST_Node) testcase = testcase.print_to_string();
minify_options = minify_options || { compress: {}, mangle: false };
reduce_options = reduce_options || {};
var max_iterations = reduce_options.max_iterations || 1000;
var max_timeout = reduce_options.max_timeout || 10000;
var verbose = reduce_options.verbose;
var minify_options_json = JSON.stringify(minify_options, null, 2);
var result_cache = Object.create(null);
// the initial timeout to assess the viability of the test case must be large
var differs = producesDifferentResultWhenMinified(result_cache, testcase, minify_options, max_timeout);
if (verbose) {
console.error("// Node.js " + process.version + " on " + os.platform() + " " + os.arch());
}
if (!differs) {
// same stdout result produced when minified
return {
code: "// Can't reproduce test failure with minify options provided:"
+ "\n// " + to_comment(minify_options_json)
};
} else if (differs.timed_out) {
return {
code: "// Can't reproduce test failure within " + max_timeout + "ms:"
+ "\n// " + to_comment(minify_options_json)
};
} else if (differs.error) {
return differs;
} else {
max_timeout = Math.min(100 * differs.elapsed, max_timeout);
// Replace expressions with constants that will be parsed into
// AST_Nodes as required. Each AST_Node has its own permutation count,
// so these replacements can't be shared.
// Although simpler replacements are generally faster and better,
// feel free to experiment with a different replacement set.
var REPLACEMENTS = [
// "null", "''", "false", "'foo'", "undefined", "9",
"1", "0",
];
// There's a relationship between each node's _permute counter and
// REPLACEMENTS.length which is why fractional _permutes were needed.
// One could scale all _permute operations by a factor of `steps`
// to only deal with integer operations, but this works well enough.
var steps = 4; // must be a power of 2
var step = 1 / steps; // 0.25 is exactly representable in floating point
var tt = new U.TreeTransformer(function(node, descend, in_list) {
if (CHANGED) return;
// quick ignores
if (node instanceof U.AST_Accessor) return;
if (node instanceof U.AST_Directive) return;
if (node instanceof U.AST_Label) return;
if (node instanceof U.AST_LabelRef) return;
if (!in_list && node instanceof U.AST_SymbolDeclaration) return;
if (node instanceof U.AST_Toplevel) return;
var parent = tt.parent();
// ensure that the _permute prop is a number.
// can not use `node.start._permute |= 0;` as it will erase fractional part.
if (typeof node.start._permute === "undefined") node.start._permute = 0;
// if node reached permutation limit - skip over it.
// no structural AST changes before this point.
if (node.start._permute >= REPLACEMENTS.length) return;
if (parent instanceof U.AST_Assign
&& parent.left === node
|| parent instanceof U.AST_Unary
&& parent.expression === node
&& ["++", "--", "delete"].indexOf(parent.operator) >= 0) {
// ignore lvalues
return;
}
if ((parent instanceof U.AST_For || parent instanceof U.AST_ForIn)
&& parent.init === node && node instanceof U.AST_Var) {
// preserve for (var ...)
return node;
}
// node specific permutations with no parent logic
if (node instanceof U.AST_Array) {
var expr = node.elements[0];
if (expr && !(expr instanceof U.AST_Hole)) {
node.start._permute++;
CHANGED = true;
return expr;
}
}
else if (node instanceof U.AST_Binary) {
CHANGED = true;
return [
node.left,
node.right,
][ ((node.start._permute += step) * steps | 0) % 2 ];
}
else if (node instanceof U.AST_Catch || node instanceof U.AST_Finally) {
// drop catch or finally block
node.start._permute++;
CHANGED = true;
return null;
}
else if (node instanceof U.AST_Conditional) {
CHANGED = true;
return [
node.condition,
node.consequent,
node.alternative,
][ ((node.start._permute += step) * steps | 0) % 3 ];
}
else if (node instanceof U.AST_BlockStatement) {
if (in_list) {
node.start._permute++;
CHANGED = true;
return List.splice(node.body);
}
}
else if (node instanceof U.AST_Call) {
var expr = [
node.expression,
node.args[0],
null, // intentional
][ ((node.start._permute += step) * steps | 0) % 3 ];
if (expr) {
CHANGED = true;
return expr;
}
if (node.expression instanceof U.AST_Function) {
// hoist and return expressions from the IIFE function expression
var body = node.expression.body;
node.expression.body = [];
var seq = [];
body.forEach(function(node) {
var expr = expr instanceof U.AST_Exit ? node.value : node.body;
if (expr instanceof U.AST_Node && !is_statement(expr)) {
// collect expressions from each statements' body
seq.push(expr);
}
});
CHANGED = true;
return to_sequence(seq);
}
}
else if (node instanceof U.AST_Defun) {
switch (((node.start._permute += step) * steps | 0) % 2) {
case 0:
CHANGED = true;
return List.skip;
case 1:
if (!has_exit(node)) {
// hoist function declaration body
var body = node.body;
node.body = [];
body.push(node); // retain function with empty body to be dropped later
CHANGED = true;
return List.splice(body);
}
}
}
else if (node instanceof U.AST_DWLoop) {
var expr = [
node.condition,
node.body,
null, // intentional
][ (node.start._permute * steps | 0) % 3 ];
node.start._permute += step;
if (!expr) {
if (node.body[0] instanceof U.AST_Break) {
if (node instanceof U.AST_Do) {
CHANGED = true;
return List.skip;
}
expr = node.condition; // AST_While - fall through
}
}
if (expr && (expr !== node.body || !has_loopcontrol(expr, node, parent))) {
CHANGED = true;
return to_statement(expr);
}
}
else if (node instanceof U.AST_PropAccess) {
var expr = [
node.expression,
node.property instanceof U.AST_Node && node.property,
][ node.start._permute++ % 2 ];
if (expr) {
CHANGED = true;
return expr;
}
}
else if (node instanceof U.AST_For) {
var expr = [
node.init,
node.condition,
node.step,
node.body,
][ (node.start._permute * steps | 0) % 4 ];
node.start._permute += step;
if (expr && (expr !== node.body || !has_loopcontrol(expr, node, parent))) {
CHANGED = true;
return to_statement(expr);
}
}
else if (node instanceof U.AST_ForIn) {
var expr = [
node.init,
node.object,
node.body,
][ (node.start._permute * steps | 0) % 3 ];
node.start._permute += step;
if (expr && (expr !== node.body || !has_loopcontrol(expr, node, parent))) {
CHANGED = true;
return to_statement(expr);
}
}
else if (node instanceof U.AST_If) {
var expr = [
node.condition,
node.body,
node.alternative,
][ (node.start._permute * steps | 0) % 3 ];
node.start._permute += step;
if (expr) {
// replace if statement with its condition, then block or else block
CHANGED = true;
return to_statement(expr);
}
}
else if (node instanceof U.AST_Object) {
// first property's value
var expr = node.properties[0] instanceof U.AST_ObjectKeyVal && node.properties[0].value;
if (expr) {
node.start._permute++;
CHANGED = true;
return expr;
}
}
else if (node instanceof U.AST_SimpleStatement) {
if (node.body instanceof U.AST_Call && node.body.expression instanceof U.AST_Function) {
// hoist simple statement IIFE function expression body
node.start._permute++;
if (!has_exit(node.body.expression)) {
var body = node.body.expression.body;
node.body.expression.body = [];
CHANGED = true;
return List.splice(body);
}
}
}
else if (node instanceof U.AST_Switch) {
var expr = [
node.expression, // switch expression
node.body[0] && node.body[0].expression, // first case expression or undefined
node.body[0] && node.body[0], // first case body or undefined
][ (node.start._permute * steps | 0) % 4 ];
node.start._permute += step;
if (expr && (!(expr instanceof U.AST_Statement) || !has_loopcontrol(expr, node, parent))) {
CHANGED = true;
return expr instanceof U.AST_SwitchBranch ? new U.AST_BlockStatement({
body: expr.body.slice(),
start: {},
}) : to_statement(expr);
}
}
else if (node instanceof U.AST_Try) {
var body = [
node.body,
node.bcatch && node.bcatch.body,
node.bfinally && node.bfinally.body,
null, // intentional
][ (node.start._permute * steps | 0) % 4 ];
node.start._permute += step;
if (body) {
// replace try statement with try block, catch block, or finally block
CHANGED = true;
return new U.AST_BlockStatement({
body: body,
start: {},
});
} else {
// replace try with a break or return if first in try statement
if (node.body[0] instanceof U.AST_Break
|| node.body[0] instanceof U.AST_Return) {
CHANGED = true;
return node.body[0];
}
}
}
else if (node instanceof U.AST_Unary) {
node.start._permute++;
CHANGED = true;
return node.expression;
}
else if (node instanceof U.AST_Var) {
if (node.definitions.length == 1 && node.definitions[0].value) {
// first declaration value
node.start._permute++;
CHANGED = true;
return to_statement(node.definitions[0].value);
}
}
else if (node instanceof U.AST_LabeledStatement) {
if (node.body instanceof U.AST_Statement
&& !has_loopcontrol(node.body, node.body, node)) {
// replace labelled statement with its non-labelled body
node.start._permute = REPLACEMENTS.length;
CHANGED = true;
return node.body;
}
}
if (in_list) {
// special case to drop object properties and switch branches
if (parent instanceof U.AST_Object
|| parent instanceof U.AST_Switch && parent.expression != node) {
node.start._permute++;
CHANGED = true;
return List.skip;
}
// replace or skip statement
if (node instanceof U.AST_Statement) {
node.start._permute++;
CHANGED = true;
return List.skip;
}
// remove this node unless its the sole element of a (transient) sequence
if (!(parent instanceof U.AST_Sequence) || parent.expressions.length > 1) {
node.start._permute++;
CHANGED = true;
return List.skip;
}
}
// replace this node
var newNode = U.parse(REPLACEMENTS[node.start._permute % REPLACEMENTS.length | 0], {
expression: true,
});
if (is_statement(node)) {
newNode = new U.AST_SimpleStatement({
body: newNode,
start: {},
});
}
newNode.start._permute = ++node.start._permute;
CHANGED = true;
return newNode;
}, function(node, in_list) {
if (node instanceof U.AST_Sequence) {
// expand single-element sequence
if (node.expressions.length == 1) return node.expressions[0];
}
else if (node instanceof U.AST_Try) {
// expand orphaned try block
if (!node.bcatch && !node.bfinally) return new U.AST_BlockStatement({
body: node.body,
start: {},
});
}
else if (node instanceof U.AST_Var) {
// remove empty var statement
if (node.definitions.length == 0) return in_list ? List.skip : new U.AST_EmptyStatement({
start: {},
});
}
});
for (var pass = 1; pass <= 3; ++pass) {
var testcase_ast = U.parse(testcase);
testcase_ast.walk(new U.TreeWalker(function(node) {
// unshare start props to retain visit data between iterations
node.start = JSON.parse(JSON.stringify(node.start));
node.start._permute = 0;
}));
for (var c = 0; c < max_iterations; ++c) {
if (verbose) {
if (pass == 1 && c % 25 == 0) {
console.error("// reduce test pass "
+ pass + ", iteration " + c + ": " + testcase.length + " bytes");
}
}
var CHANGED = false;
var code_ast = testcase_ast.clone(true).transform(tt);
if (!CHANGED) break;
try {
var code = code_ast.print_to_string();
} catch (ex) {
// AST is not well formed.
// no harm done - just log the error, ignore latest change and continue iterating.
console.error("*** Error generating code from AST.");
console.error(ex);
console.error("*** Discarding permutation and continuing.");
continue;
}
var diff = producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout);
if (diff) {
if (diff.timed_out) {
// can't trust the validity of `code_ast` and `code` when timed out.
// no harm done - just ignore latest change and continue iterating.
} else if (diff.error) {
// something went wrong during minify() - could be malformed AST or genuine bug.
// no harm done - just log code & error, ignore latest change and continue iterating.
console.error("*** Error during minification.");
console.error(code);
console.error(diff.error);
console.error("*** Discarding permutation and continuing.");
} else if (is_error(diff.unminified_result)
&& is_error(diff.minified_result)
&& diff.unminified_result.name == diff.minified_result.name) {
// ignore difference in error messages caused by minification
} else {
// latest permutation is valid, so use it as the basis of new changes
testcase_ast = code_ast;
testcase = code;
differs = diff;
}
}
}
if (c == 0) break;
if (verbose) {
console.error("// reduce test pass " + pass + ": " + testcase.length + " bytes");
}
}
testcase = U.minify(testcase, {
compress: false,
mangle: false,
output: {
beautify: true,
braces: true,
comments: true,
},
});
testcase.code += [
"",
"// output: " + to_comment(differs.unminified_result),
"// minify: " + to_comment(differs.minified_result),
"// options: " + to_comment(minify_options_json),
].join("\n").replace(/\u001b\[\d+m/g, "");
return testcase;
}
};
function to_comment(value) {
return ("" + value).replace(/\n/g, "\n// ");
}
function has_exit(fn) {
var found = false;
var tw = new U.TreeWalker(function(node) {
if (found) return found;
if (node instanceof U.AST_Exit) {
return found = true;
}
if (node instanceof U.AST_Scope && node !== fn) {
return true; // don't descend into nested functions
}
});
fn.walk(tw);
return found;
}
function has_loopcontrol(body, loop, label) {
var found = false;
var tw = new U.TreeWalker(function(node) {
if (found) return true;
if (node instanceof U.AST_LoopControl && this.loopcontrol_target(node) === loop) {
return found = true;
}
});
if (label instanceof U.AST_LabeledStatement) tw.push(label);
tw.push(loop);
body.walk(tw);
return found;
}
function is_error(result) {
return typeof result == "object" && typeof result.name == "string" && typeof result.message == "string";
}
function is_timed_out(result) {
return is_error(result) && /timed out/.test(result);
}
function is_statement(node) {
return node instanceof U.AST_Statement && !(node instanceof U.AST_Function);
}
function merge_sequence(array, node) {
if (node instanceof U.AST_Sequence) {
array.push.apply(array, node.expressions);
} else {
array.push(node);
}
return array;
}
function to_sequence(expressions) {
if (expressions.length == 0) return new U.AST_Number({value: 0, start: {}});
if (expressions.length == 1) return expressions[0];
return new U.AST_Sequence({
expressions: expressions.reduce(merge_sequence, []),
start: {},
});
}
function to_statement(node) {
return is_statement(node) ? node : new U.AST_SimpleStatement({
body: node,
start: {},
});
}
function run_code(result_cache, code, toplevel, timeout) {
var key = crypto.createHash("sha1").update(code).digest("base64");
return result_cache[key] || (result_cache[key] = sandbox.run_code(code, toplevel, timeout));
}
function producesDifferentResultWhenMinified(result_cache, code, minify_options, max_timeout) {
var minified = U.minify(code, minify_options);
if (minified.error) return minified;
var toplevel = minify_options.toplevel;
var elapsed = Date.now();
var unminified_result = run_code(result_cache, code, toplevel, max_timeout);
elapsed = Date.now() - elapsed;
var timeout = Math.min(100 * elapsed, max_timeout);
var minified_result = run_code(result_cache, minified.code, toplevel, timeout);
if (sandbox.same_stdout(unminified_result, minified_result)) {
return is_timed_out(unminified_result) && is_timed_out(minified_result) && {
timed_out: true,
};
}
return {
unminified_result: unminified_result,
minified_result: minified_result,
elapsed: elapsed,
};
}
Error.stackTraceLimit = Infinity;

View File

@@ -54,14 +54,15 @@ function createContext() {
}
}
exports.run_code = function(code, toplevel) {
exports.run_code = function(code, toplevel, timeout) {
timeout = timeout || 5000;
var stdout = "";
var original_write = process.stdout.write;
process.stdout.write = function(chunk) {
stdout += chunk;
};
try {
vm.runInContext(toplevel ? "(function(){" + code + "})()" : code, createContext(), { timeout: 5000 });
vm.runInContext(toplevel ? "(function(){" + code + "})()" : code, createContext(), { timeout: timeout });
return stdout;
} catch (ex) {
return ex;
@@ -76,8 +77,9 @@ function strip_func_ids(text) {
exports.same_stdout = semver.satisfies(process.version, "0.12") ? function(expected, actual) {
if (typeof expected != typeof actual) return false;
if (typeof expected != "string") {
if (expected.name != actual.name) return false;
if (typeof expected == "object" && typeof expected.name == "string" && typeof expected.message == "string") {
if (expected.name !== actual.name) return false;
if (typeof actual.message != "string") return false;
expected = expected.message.slice(expected.message.lastIndexOf("\n") + 1);
actual = actual.message.slice(actual.message.lastIndexOf("\n") + 1);
}

View File

@@ -10,6 +10,7 @@ require("../../tools/exit");
var UglifyJS = require("../..");
var randomBytes = require("crypto").randomBytes;
var sandbox = require("../sandbox");
var reduce_test = require("../reduce");
var MAX_GENERATED_TOPLEVELS_PER_RUN = 1;
var MAX_GENERATION_RECURSION_DEPTH = 12;
@@ -1051,6 +1052,7 @@ function log_rename(options) {
}
function log(options) {
var options_copy = JSON.parse(options);
options = JSON.parse(options);
if (!ok) errorln("\n\n\n\n\n\n!!!!!!!!!!\n\n\n");
errorln("//=============================================================");
@@ -1069,6 +1071,18 @@ function log(options) {
errorln(original_result);
errorln("uglified result:");
errorln(uglify_result);
errorln("//-------------------------------------------------------------");
var reduced = reduce_test(original_code, options_copy, {
verbose: false,
}).code;
if (reduced) {
errorln();
errorln("// reduced test case (output will differ)");
errorln();
errorln(reduced);
errorln();
errorln("//-------------------------------------------------------------");
}
} else {
errorln("// !!! uglify failed !!!");
errorln(uglify_code);

View File

@@ -1,4 +1,5 @@
exports["Dictionary"] = Dictionary;
exports["List"] = List;
exports["minify"] = minify;
exports["parse"] = parse;
exports["push_uniq"] = push_uniq;