From: Mihai Bazon Date: Wed, 22 Aug 2012 12:21:58 +0000 (+0300) Subject: wrote more of the compressor and added some tests X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=159a6f048cfabc6bdee0bb1274eeb0d6ab75b79a;p=UglifyJS.git wrote more of the compressor and added some tests --- 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) {