aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/ast.js7
-rw-r--r--lib/compress.js149
-rw-r--r--test/compress/blocks.js49
-rw-r--r--test/compress/dead-code.js53
-rw-r--r--test/compress/debugger.js24
-rw-r--r--test/compress/properties.js25
-rw-r--r--test/compress/sequences.js60
-rwxr-xr-xtest/run-tests.js144
-rwxr-xr-xtmp/test-node.js2
-rw-r--r--tools/node.js3
10 files changed, 497 insertions, 19 deletions
diff --git a/lib/ast.js b/lib/ast.js
index 97480cc0..ff662d4a 100644
--- a/lib/ast.js
+++ b/lib/ast.js
@@ -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)];
}
};
diff --git a/lib/compress.js b/lib/compress.js
index c28aacce..89c128d0 100644
--- a/lib/compress.js
+++ b/lib/compress.js
@@ -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;
});
diff --git a/test/compress/blocks.js b/test/compress/blocks.js
new file mode 100644
index 00000000..027b5d64
--- /dev/null
+++ b/test/compress/blocks.js
@@ -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();
+ }
+}
diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js
new file mode 100644
index 00000000..bb955569
--- /dev/null
+++ b/test/compress/dead-code.js
@@ -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(){};
+ }
+ }
+}
diff --git a/test/compress/debugger.js b/test/compress/debugger.js
new file mode 100644
index 00000000..7c270734
--- /dev/null
+++ b/test/compress/debugger.js
@@ -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);
+ }
+}
diff --git a/test/compress/properties.js b/test/compress/properties.js
new file mode 100644
index 00000000..72e245ec
--- /dev/null
+++ b/test/compress/properties.js
@@ -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";
+ }
+}
diff --git a/test/compress/sequences.js b/test/compress/sequences.js
new file mode 100644
index 00000000..ec0f4c97
--- /dev/null
+++ b/test/compress/sequences.js
@@ -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();
+ }
+ }
+}
diff --git a/test/run-tests.js b/test/run-tests.js
new file mode 100755
index 00000000..b6e8ee2b
--- /dev/null
+++ b/test/run-tests.js
@@ -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 + ")")();
+}
diff --git a/tmp/test-node.js b/tmp/test-node.js
index 377d0017..c6ab5031 100755
--- a/tmp/test-node.js
+++ b/tmp/test-node.js
@@ -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");
diff --git a/tools/node.js b/tools/node.js
index b533e419..d61b44a7 100644
--- a/tools/node.js
+++ b/tools/node.js
@@ -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) {