wrote more of the compressor and added some tests
authorMihai Bazon <mihai@bazon.net>
Wed, 22 Aug 2012 12:21:58 +0000 (15:21 +0300)
committerMihai Bazon <mihai@bazon.net>
Wed, 22 Aug 2012 12:21:58 +0000 (15:21 +0300)
lib/ast.js
lib/compress.js
test/compress/blocks.js [new file with mode: 0644]
test/compress/dead-code.js [new file with mode: 0644]
test/compress/debugger.js [new file with mode: 0644]
test/compress/properties.js [new file with mode: 0644]
test/compress/sequences.js [new file with mode: 0644]
test/run-tests.js [new file with mode: 0755]
tmp/test-node.js
tools/node.js

index 97480cc..ff662d4 100644 (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)];
     }
 };
index c28aacc..89c128d 100644 (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;
     });
 
diff --git a/test/compress/blocks.js b/test/compress/blocks.js
new file mode 100644 (file)
index 0000000..027b5d6
--- /dev/null
@@ -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 (file)
index 0000000..bb95556
--- /dev/null
@@ -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 (file)
index 0000000..7c27073
--- /dev/null
@@ -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 (file)
index 0000000..72e245e
--- /dev/null
@@ -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 (file)
index 0000000..ec0f4c9
--- /dev/null
@@ -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 (executable)
index 0000000..b6e8ee2
--- /dev/null
@@ -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 + ")")();
+}
index 377d001..c6ab503 100755 (executable)
@@ -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");
index b533e41..d61b44a 100644 (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) {