wrote more of the compressor and added some tests

This commit is contained in:
Mihai Bazon
2012-08-22 15:21:58 +03:00
parent f53e139d3c
commit 159a6f048c
10 changed files with 497 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ function DEFNODE(type, props, methods, base) {
if (arguments.length < 4) base = AST_Node;
if (!props) props = [];
else props = props.split(/\s+/);
var self_props = props;
if (base && base.PROPS)
props = props.concat(base.PROPS);
var code = "return function AST_" + type + "(props){ if (props) { ";
@@ -19,6 +20,7 @@ function DEFNODE(type, props, methods, base) {
}
ctor.prototype.CTOR = ctor;
ctor.PROPS = props || null;
ctor.SELF_PROPS = self_props;
if (type) {
ctor.prototype.TYPE = ctor.TYPE = type;
}
@@ -563,7 +565,10 @@ TreeWalker.prototype = {
if (!ret && descend) {
descend.call(node);
}
this.stack.pop(node);
this.stack.pop();
return ret;
},
parent: function(n) {
return this.stack[this.stack.length - 2 - (n || 0)];
}
};

View File

@@ -9,13 +9,16 @@
// maintaining various internal state that might be useful for
// squeezing nodes.
function Compressor(options) {
function Compressor(options, false_by_default) {
options = defaults(options, {
sequences : true,
dead_code : true,
keep_comps : true,
drop_debugger : true,
unsafe : true
sequences : !false_by_default,
properties : !false_by_default,
dead_code : !false_by_default,
keep_comps : !false_by_default,
drop_debugger : !false_by_default,
unsafe : !false_by_default,
warnings : true
});
var stack = [];
return {
@@ -25,6 +28,10 @@ function Compressor(options) {
stack : function() { return stack },
parent : function(n) {
return stack[stack.length - 2 - (n || 0)];
},
warn : function() {
if (options.warnings)
AST_Node.warn.apply(AST_Node, arguments);
}
};
};
@@ -35,6 +42,12 @@ function Compressor(options) {
return this;
});
function make_node(ctor, orig, props) {
if (!props.start) props.start = orig.start;
if (!props.end) props.end = orig.end;
return new ctor(props);
};
function SQUEEZE(nodetype, squeeze) {
nodetype.DEFMETHOD("squeeze", function(compressor){
compressor.push_node(this);
@@ -46,11 +59,6 @@ function Compressor(options) {
function do_list(array, compressor) {
return MAP(array, function(node){
if (node instanceof Array) {
sys.debug(node.map(function(node){
return node.TYPE;
}).join("\n"));
}
return node.squeeze(compressor);
});
};
@@ -72,15 +80,109 @@ function Compressor(options) {
return self;
});
function tighten_body(statements, compressor) {
statements = do_list(statements, compressor);
statements = eliminate_spurious_blocks(statements);
if (compressor.option("dead_code")) {
statements = eliminate_dead_code(statements, compressor);
}
if (compressor.option("sequences")) {
statements = sequencesize(statements);
}
return statements;
};
function eliminate_spurious_blocks(statements) {
return statements.reduce(function(a, stat){
if (stat.TYPE == "BlockStatement") {
// XXX: no instanceof here because we would catch
// AST_Lambda-s and other blocks too. perhaps we
// should refine the hierarchy.
a.push.apply(a, stat.body);
} else {
a.push(stat);
}
return a;
}, []);
}
function eliminate_dead_code(statements, compressor) {
var has_quit = false;
return statements.reduce(function(a, stat){
if (has_quit) {
if (stat instanceof AST_Defun) {
a.push(stat);
}
else if (compressor.option("warnings")) {
stat.walk(new TreeWalker(function(node){
if (node instanceof AST_Definitions
|| node instanceof AST_Defun) {
compressor.warn("Declarations in unreachable code! [{line},{col}]", node.start);
if (node instanceof AST_Definitions) {
node = node.clone();
node.remove_initializers();
a.push(node);
}
else if (node instanceof AST_Defun) {
a.push(node);
}
return true;
}
if (node instanceof AST_Scope)
return true;
}))
};
} else {
a.push(stat);
if (stat instanceof AST_Jump) {
has_quit = true;
}
}
return a;
}, []);
}
function sequencesize(statements) {
var prev = null, last = statements.length - 1;
if (last) statements = statements.reduce(function(a, cur, i){
if (prev instanceof AST_SimpleStatement
&& cur instanceof AST_SimpleStatement) {
var seq = make_node(AST_Seq, prev, {
first: prev.body,
second: cur.body
});
prev.body = seq;
}
else if (i == last && cur instanceof AST_Exit
&& cur.value && a.length == 1) {
// it only makes sense to do this transformation
// if the AST gets to a single statement.
var seq = make_node(AST_Seq, prev, {
first: prev.body,
second: cur.value
});
cur.value = seq;
return [ cur ];
}
else {
a.push(cur);
prev = cur;
}
return a;
}, []);
return statements;
}
SQUEEZE(AST_BlockStatement, function(self, compressor){
self = self.clone();
self.body = do_list(self.body, compressor);
self.body = tighten_body(self.body, compressor);
if (self.body.length == 1 && !self.required)
return self.body[0];
return self;
});
SQUEEZE(AST_EmptyStatement, function(self, compressor){
if (compressor.parent() instanceof AST_BlockStatement)
return MAP.skip;
return self;
});
SQUEEZE(AST_DWLoop, function(self, compressor){
@@ -144,7 +246,7 @@ function Compressor(options) {
SQUEEZE(AST_Case, function(self, compressor){
self = self.clone();
self.expression = self.expression.squeeze(compressor);
self.body = do_list(self.body, compressor);
self.body = tighten_body(self.body, compressor);
return self;
});
@@ -156,6 +258,14 @@ function Compressor(options) {
return self;
});
AST_Definitions.DEFMETHOD("remove_initializers", function(){
this.definitions = this.definitions.map(function(def){
var def = def.clone();
def.value = null;
return def;
});
});
SQUEEZE(AST_Definitions, function(self, compressor){
self = self.clone();
self.definitions = do_list(self.definitions, compressor);
@@ -199,7 +309,14 @@ function Compressor(options) {
SQUEEZE(AST_Sub, function(self, compressor){
self = self.clone();
self.expression = self.expression.squeeze(compressor);
self.property = self.property.squeeze(compressor);
var prop = self.property = self.property.squeeze(compressor);
if (prop instanceof AST_String && compressor.option("properties")) {
prop = prop.getValue();
if (is_identifier(prop)) {
self = new AST_Dot(self);
self.property = prop;
}
}
return self;
});

49
test/compress/blocks.js Normal file
View File

@@ -0,0 +1,49 @@
remove_blocks: {
input: {
{;}
foo();
{};
{
{};
};
bar();
{}
}
expect: {
foo();
bar();
}
}
keep_some_blocks: {
input: {
// 1.
if (foo) {
{{{}}}
if (bar) baz();
{{}}
} else {
stuff();
}
// 2.
if (foo) {
for (var i = 0; i < 5; ++i)
if (bar) baz();
} else {
stuff();
}
}
expect: {
// 1.
if (foo) {
if (bar) baz();
} else stuff();
// 2.
if (foo) {
for (var i = 0; i < 5; ++i)
if (bar) baz();
} else stuff();
}
}

View File

@@ -0,0 +1,53 @@
dead_code_1: {
options = {
dead_code: true
};
input: {
function f() {
a();
b();
x = 10;
return;
if (x) {
y();
}
}
}
expect: {
function f() {
a();
b();
x = 10;
return;
}
}
}
dead_code_2_should_warn: {
options = {
dead_code: true
};
input: {
function f() {
g();
x = 10;
throw "foo";
// completely discarding the `if` would introduce some
// bugs. UglifyJS v1 doesn't deal with this issue.
if (x) {
y();
var x;
function g(){};
}
}
}
expect: {
function f() {
g();
x = 10;
throw "foo";
var x;
function g(){};
}
}
}

24
test/compress/debugger.js Normal file
View File

@@ -0,0 +1,24 @@
keep_debugger: {
options = {
drop_debugger: false
};
input: {
debugger;
}
expect: {
debugger;
}
}
drop_debugger: {
options = {
drop_debugger: true
};
input: {
debugger;
if (foo) debugger;
}
expect: {
if (foo);
}
}

View File

@@ -0,0 +1,25 @@
keep_properties: {
options = {
properties: false
};
input: {
a["foo"] = "bar";
}
expect: {
a["foo"] = "bar";
}
}
dot_properties: {
options = {
properties: true
};
input: {
a["foo"] = "bar";
a["if"] = "if";
}
expect: {
a.foo = "bar";
a["if"] = "if";
}
}

View File

@@ -0,0 +1,60 @@
make_sequences_1: {
options = {
sequences: true
};
input: {
foo();
bar();
baz();
}
expect: {
foo(),bar(),baz();
}
}
make_sequences_2: {
options = {
sequences: true
};
input: {
if (boo) {
foo();
bar();
baz();
} else {
x();
y();
z();
}
}
expect: {
if (boo) foo(),bar(),baz();
else x(),y(),z();
}
}
make_sequences_3: {
options = {
sequences: true
};
input: {
function f() {
foo();
bar();
return baz();
}
function g() {
foo();
bar();
throw new Error();
}
}
expect: {
function f() {
return foo(), bar(), baz();
}
function g() {
throw foo(), bar(), new Error();
}
}
}

144
test/run-tests.js Executable file
View File

@@ -0,0 +1,144 @@
#! /usr/bin/env node
var U = require("../tools/node");
var path = require("path");
var fs = require("fs");
var assert = require("assert");
var sys = require("util");
var tests_dir = path.dirname(module.filename);
run_compress_tests();
/* -----[ utils ]----- */
function tmpl() {
return U.string_template.apply(this, arguments);
}
function log() {
var txt = tmpl.apply(this, arguments);
sys.puts(txt);
}
function log_directory(dir) {
log("--- Entering [{dir}]", { dir: dir });
}
function log_start_file(file) {
log("*** {file}", { file: file });
}
function log_test(name) {
log(" Running test [{name}]", { name: name });
}
function find_test_files(dir) {
var files = fs.readdirSync(dir).filter(function(name){
return /\.js$/i.test(name);
});
return files;
}
function test_directory(dir) {
return path.resolve(tests_dir, dir);
}
function run_compress_tests() {
var dir = test_directory("compress");
log_directory("compress");
var files = find_test_files(dir);
function test_file(file) {
log_start_file(file);
function test_case(test) {
log_test(test.name);
var cmp = new U.Compressor(test.options || {}, true);
var expect = make_code(test.expect, false);
var output = make_code(test.input.squeeze(cmp), false);
if (expect != output) {
log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", {
input: make_code(test.input),
output: output,
expected: expect
});
}
}
var tests = parse_test(path.resolve(dir, file));
for (var i in tests) if (tests.hasOwnProperty(i)) {
test_case(tests[i]);
}
}
files.forEach(function(file){
test_file(file);
});
}
function parse_test(file) {
var script = fs.readFileSync(file, "utf8");
var ast = U.parse(script);
var tests = {};
var tw = new U.TreeWalker(function(node, descend){
if (node instanceof U.AST_LabeledStatement
&& tw.parent() instanceof U.AST_Toplevel) {
var name = node.label.name;
tests[name] = get_one_test(name, node.statement);
return true;
}
if (!(node instanceof U.AST_Toplevel)) croak(node);
});
ast.walk(tw);
return tests;
function croak(node) {
throw new Error(tmpl("Can't understand test file {file} [{line},{col}]\n{code}", {
file: file,
line: node.start.line,
col: node.start.col,
code: make_code(node, false)
}));
}
function get_one_test(name, block) {
var test = { name: name, options: {} };
var tw = new U.TreeWalker(function(node, descend){
if (node instanceof U.AST_Assign) {
if (!(node.left instanceof U.AST_SymbolRef)) {
croak(node);
}
var name = node.left.name;
test[name] = evaluate(node.right);
return true;
}
if (node instanceof U.AST_LabeledStatement) {
assert.ok(
node.label.name == "input" || node.label.name == "expect",
tmpl("Unsupported label {name} [{line},{col}]", {
name: node.label.name,
line: node.label.start.line,
col: node.label.start.col
})
);
var stat = node.statement;
if (stat instanceof U.AST_BlockStatement)
stat.required = 1;
test[node.label.name] = stat;
return true;
}
});
block.walk(tw);
return test;
};
}
function make_code(ast, beautify) {
if (arguments.length == 1) beautify = true;
var stream = U.OutputStream({ beautify: beautify });
ast.print(stream);
return stream.get();
}
function evaluate(code) {
if (code instanceof U.AST_Node)
code = make_code(code);
return new Function("return(" + code + ")")();
}

View File

@@ -3,7 +3,7 @@
var sys = require("util");
var fs = require("fs");
var UglifyJS = require("../tools/node.js");
var UglifyJS = require("../tools/node");
var filename = process.argv[2];
var code = fs.readFileSync(filename, "utf8");

View File

@@ -4,7 +4,8 @@ var sys = require("util");
var path = require("path");
var UglifyJS = vm.createContext({
sys: sys
sys : sys,
console : console
});
function load_global(file) {