From 6f3e35bb3f04303e6b7cc74cfc25bfee3c792a98 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 27 Dec 2015 22:24:37 +0100 Subject: [PATCH 01/51] Fix ch that could contain other newline characters --- lib/parse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 7e7b2272..2218c00c 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -399,7 +399,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { if (octal_len > 0) ch = String.fromCharCode(parseInt(ch, 8)); else ch = read_escaped_char(true); } - else if (ch == "\n") parse_error("Unterminated string constant"); + else if ("\r\n\u2028\u2029".indexOf(ch) >= 0) parse_error("Unterminated string constant"); else if (ch == quote) break; ret += ch; } From 8c6af09ae014eb2370349fb7b419ee912abac64f Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 27 Dec 2015 22:28:03 +0100 Subject: [PATCH 02/51] Add mocha tests --- package.json | 3 ++- test/mocha.js | 29 +++++++++++++++++++++++++++++ test/mocha/string-literal.js | 29 +++++++++++++++++++++++++++++ test/run-tests.js | 3 +++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 test/mocha.js create mode 100644 test/mocha/string-literal.js diff --git a/package.json b/package.json index 6b0d2f40..bd4fb3e7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "acorn": "~0.6.0", "escodegen": "~1.3.3", "esfuzz": "~0.3.1", - "estraverse": "~1.5.1" + "estraverse": "~1.5.1", + "mocha": "~2.3.4" }, "browserify": { "transform": [ diff --git a/test/mocha.js b/test/mocha.js new file mode 100644 index 00000000..411f52c5 --- /dev/null +++ b/test/mocha.js @@ -0,0 +1,29 @@ +var Mocha = require('mocha'), + fs = require('fs'), + path = require('path'); + +// Instantiate a Mocha instance. +var mocha = new Mocha({}); + +var testDir = __dirname + '/mocha/'; + +// Add each .js file to the mocha instance +fs.readdirSync(testDir).filter(function(file){ + // Only keep the .js files + return file.substr(-3) === '.js'; + +}).forEach(function(file){ + mocha.addFile( + path.join(testDir, file) + ); +}); + +module.exports = function() { + mocha.run(function(failures) { + if (failures !== 0) { + process.on('exit', function () { + process.exit(failures); + }); + } + }); +}; \ No newline at end of file diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js new file mode 100644 index 00000000..64933632 --- /dev/null +++ b/test/mocha/string-literal.js @@ -0,0 +1,29 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("String literals", function() { + it("Should throw syntax error if a string literal contains a newline", function() { + var inputs = [ + "'\n'", + "'\r'", + '"\r\n"', + "'\u2028'", + '"\u2029"' + ]; + + var test = function(input) { + return function() { + var ast = UglifyJS.parse(input); + } + }; + + var error = function(e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Unterminated string constant" + }; + + for (var input in inputs) { + assert.throws(test(inputs[input]), error); + } + }); +}); \ No newline at end of file diff --git a/test/run-tests.js b/test/run-tests.js index 3ec04fda..b9a0f825 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -16,6 +16,9 @@ if (failures) { process.exit(1); } +var mocha_tests = require("./mocha.js"); +mocha_tests(); + var run_sourcemaps_tests = require('./sourcemaps'); run_sourcemaps_tests(); From fe4e9f9d97dcc6594a8fc49e04630aa619ff1866 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Tue, 5 Jan 2016 13:56:52 +0200 Subject: [PATCH 03/51] Fix hoisting the var in ForIn Close #913 --- lib/compress.js | 5 ++++- test/compress/issue-913.js | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-913.js diff --git a/lib/compress.js b/lib/compress.js index 44e19799..1f5988f7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1276,7 +1276,10 @@ merge(Compressor.prototype, { var seq = node.to_assignments(); var p = tt.parent(); if (p instanceof AST_ForIn && p.init === node) { - if (seq == null) return node.definitions[0].name; + if (seq == null) { + var def = node.definitions[0].name; + return make_node(AST_SymbolRef, def, def); + } return seq; } if (p instanceof AST_For && p.init === node) { diff --git a/test/compress/issue-913.js b/test/compress/issue-913.js new file mode 100644 index 00000000..9d34d9d9 --- /dev/null +++ b/test/compress/issue-913.js @@ -0,0 +1,20 @@ +keep_var_for_in: { + options = { + hoist_vars: true, + unused: true + }; + input: { + (function(obj){ + var foo = 5; + for (var i in obj) + return foo; + })(); + } + expect: { + (function(obj){ + var i, foo = 5; + for (i in obj) + return foo; + })(); + } +} From 88b77ddaa9d6b3d55e537dc21030ac58ddfcb86e Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Wed, 13 Jan 2016 00:30:32 +0100 Subject: [PATCH 04/51] Add test case for line continuation --- test/mocha/string-literal.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/mocha/string-literal.js b/test/mocha/string-literal.js index 64933632..84aaad7e 100644 --- a/test/mocha/string-literal.js +++ b/test/mocha/string-literal.js @@ -14,16 +14,21 @@ describe("String literals", function() { var test = function(input) { return function() { var ast = UglifyJS.parse(input); - } + }; }; var error = function(e) { return e instanceof UglifyJS.JS_Parse_Error && - e.message === "Unterminated string constant" + e.message === "Unterminated string constant"; }; for (var input in inputs) { assert.throws(test(inputs[input]), error); } }); + + it("Should not throw syntax error if a string has a line continuation", function() { + var output = UglifyJS.parse('var a = "a\\\nb";').print_to_string(); + assert.equal(output, 'var a="ab";'); + }); }); \ No newline at end of file From 6605d1578351939ee0e39a13bf68cc9c1708c918 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sun, 10 Jan 2016 23:33:54 +0100 Subject: [PATCH 05/51] Never mangle arguments and keep them in their scope Fixes #892 Helped-by: kzc --- lib/scope.js | 8 ++++++++ test/compress/issue-892.js | 32 ++++++++++++++++++++++++++++++++ test/run-tests.js | 4 ++++ 3 files changed, 44 insertions(+) create mode 100644 test/compress/issue-892.js diff --git a/lib/scope.js b/lib/scope.js index 1f0986c4..5e93020f 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -237,6 +237,10 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; + + var symbol = new AST_VarDef({ name: "arguments" }); + var def = new SymbolDef(this, this.variables.size(), symbol); + this.variables.set(symbol.name, def); }); AST_SymbolRef.DEFMETHOD("reference", function() { @@ -366,6 +370,10 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ options = this._default_mangler_options(options); + + // Never mangle arguments + options.except.push('arguments'); + // We only need to mangle declaration nodes. Special logic wired // into the code generator will display the mangled name if it's // present (and for AST_SymbolRef-s it'll use the mangled name of diff --git a/test/compress/issue-892.js b/test/compress/issue-892.js new file mode 100644 index 00000000..2dab420f --- /dev/null +++ b/test/compress/issue-892.js @@ -0,0 +1,32 @@ +dont_mangle_arguments: { + mangle = { + }; + options = { + sequences : true, + properties : true, + dead_code : true, + drop_debugger : true, + conditionals : true, + comparisons : true, + evaluate : true, + booleans : true, + loops : true, + unused : true, + hoist_funs : true, + keep_fargs : true, + keep_fnames : false, + hoist_vars : true, + if_return : true, + join_vars : true, + cascade : true, + side_effects : true, + negate_iife : false + }; + input: { + (function(){ + var arguments = arguments, not_arguments = 9; + console.log(not_arguments, arguments); + })(5,6,7); + } + expect_exact: "(function(){var arguments=arguments,o=9;console.log(o,arguments)})(5,6,7);" +} diff --git a/test/run-tests.js b/test/run-tests.js index b9a0f825..fcb1b375 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -103,6 +103,10 @@ function run_compress_tests() { } var output = input.transform(cmp); output.figure_out_scope(); + if (test.mangle) { + output.compute_char_frequency(test.mangle); + output.mangle_names(test.mangle); + } output = make_code(output, output_options); if (expect != output) { log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", { From 5c4e470d43438e359fbdb93950e5d37d4df45a69 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Thu, 14 Jan 2016 22:32:46 +0100 Subject: [PATCH 06/51] Add scope test for arguments --- test/mocha/arguments.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 test/mocha/arguments.js diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js new file mode 100644 index 00000000..294a6c16 --- /dev/null +++ b/test/mocha/arguments.js @@ -0,0 +1,19 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("arguments", function() { + it("Should known that arguments in functions are local scoped", function() { + var ast = UglifyJS.parse("var arguments; var f = function() {arguments.length}"); + ast.figure_out_scope(); + + // Select symbol in function + var symbol = ast.body[1].definitions[0].value.find_variable("arguments"); + + assert.strictEqual(symbol.global, false); + assert.strictEqual(symbol.scope, ast. // From ast + body[1]. // Select 2nd statement (equals to `var f ...`) + definitions[0]. // First definition of selected statement + value // Select function as scope + ); + }); +}); \ No newline at end of file From 8439c8ba9813faaea062b64306cbd0b2a448bb20 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 15 Jan 2016 00:04:05 +0100 Subject: [PATCH 07/51] Make arguments test slightly more strict --- test/mocha/arguments.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/mocha/arguments.js b/test/mocha/arguments.js index 294a6c16..089826fc 100644 --- a/test/mocha/arguments.js +++ b/test/mocha/arguments.js @@ -6,7 +6,10 @@ describe("arguments", function() { var ast = UglifyJS.parse("var arguments; var f = function() {arguments.length}"); ast.figure_out_scope(); - // Select symbol in function + // Test scope of `var arguments` + assert.strictEqual(ast.find_variable("arguments").global, true); + + // Select arguments symbol in function var symbol = ast.body[1].definitions[0].value.find_variable("arguments"); assert.strictEqual(symbol.global, false); From 70e5b6f15b130eb1366ff81e0a8a7f187e9cf427 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Tue, 19 Jan 2016 14:00:22 +0100 Subject: [PATCH 08/51] Add some tests for comment-filters through api Also never bother comment options to filter comment5/shebang comments as they have their custom filter. --- lib/output.js | 4 ++-- test/mocha/comment-filter.js | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 test/mocha/comment-filter.js diff --git a/lib/output.js b/lib/output.js index f10c918a..dceece34 100644 --- a/lib/output.js +++ b/lib/output.js @@ -444,11 +444,11 @@ function OutputStream(options) { }); } else if (c.test) { comments = comments.filter(function(comment){ - return c.test(comment.value) || comment.type == "comment5"; + return comment.type == "comment5" || c.test(comment.value); }); } else if (typeof c == "function") { comments = comments.filter(function(comment){ - return c(self, comment) || comment.type == "comment5"; + return comment.type == "comment5" || c(self, comment); }); } diff --git a/test/mocha/comment-filter.js b/test/mocha/comment-filter.js new file mode 100644 index 00000000..ea2ec2eb --- /dev/null +++ b/test/mocha/comment-filter.js @@ -0,0 +1,45 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("comment filters", function() { + it("Should be able to filter comments by passing regex", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + assert.strictEqual(ast.print_to_string({comments: /^!/}), "/*!test1*/\n//!test3\n//!test6\n//!test8\n"); + }); + + it("Should be able to filter comments by passing a function", function() { + var ast = UglifyJS.parse("/*TEST 123*/\n//An other comment\n//8 chars."); + var f = function(node, comment) { + return comment.value.length === 8; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "/*TEST 123*/\n//8 chars.\n"); + }); + + it("Should be able to get the comment and comment type when using a function", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + var f = function(node, comment) { + return comment.type == "comment1" || comment.type == "comment3"; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "//!test3\n//test4\n//test5\n//!test6\n"); + }); + + it("Should be able to filter comments by passing a boolean", function() { + var ast = UglifyJS.parse("/*!test1*/\n/*test2*/\n//!test3\n//test4\ntest7\n-->!test8"); + + assert.strictEqual(ast.print_to_string({comments: true}), "/*!test1*/\n/*test2*/\n//!test3\n//test4\n//test5\n//!test6\n//test7\n//!test8\n"); + assert.strictEqual(ast.print_to_string({comments: false}), ""); + }); + + it("Should never be able to filter comment5 (shebangs)", function() { + var ast = UglifyJS.parse("#!Random comment\n//test1\n/*test2*/"); + var f = function(node, comment) { + assert.strictEqual(comment.type === "comment5", false); + + return true; + }; + + assert.strictEqual(ast.print_to_string({comments: f}), "#!Random comment\n//test1\n/*test2*/\n"); + }); +}); From ebe118dc79aca8b143409f13b336c2a04f028fa4 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Sat, 16 Jan 2016 21:35:39 +0100 Subject: [PATCH 09/51] Add keywords to package.json Should hopefully bump up on the results of the npm site when searching `uglify` --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bd4fb3e7..da0f835e 100644 --- a/package.json +++ b/package.json @@ -49,5 +49,6 @@ "scripts": { "shrinkwrap": "rm ./npm-shrinkwrap.json; rm -rf ./node_modules; npm i && npm shrinkwrap && npm outdated", "test": "node test/run-tests.js" - } + }, + "keywords": ["uglify", "uglify-js", "minify", "minifier"] } From 26641f3fb20bce9394c3989bea0099dcd209be61 Mon Sep 17 00:00:00 2001 From: Anthony Van de Gejuchte Date: Fri, 15 Jan 2016 15:58:15 +0100 Subject: [PATCH 10/51] Allow operator names as getters/setters Fixes #919 Fix provided by @kzc --- lib/parse.js | 7 +++ test/compress/issue-12.js | 47 ++++++++++++++++++++ test/mocha/getter-setter.js | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 test/mocha/getter-setter.js diff --git a/lib/parse.js b/lib/parse.js index 2218c00c..f1495153 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1178,6 +1178,13 @@ function parse($TEXT, options) { break; } break; + case "operator": + if (!is_identifier_string(tok.value)) { + throw new JS_Parse_Error("Invalid getter/setter name: " + tok.value, + tok.file, tok.line, tok.col, tok.pos); + } + ret = _make_symbol(AST_SymbolRef); + break; } next(); return ret; diff --git a/test/compress/issue-12.js b/test/compress/issue-12.js index bf87d5c0..e2d8bda7 100644 --- a/test/compress/issue-12.js +++ b/test/compress/issue-12.js @@ -9,3 +9,50 @@ keep_name_of_setter: { input: { a = { set foo () {} } } expect: { a = { set foo () {} } } } + +setter_with_operator_keys: { + input: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } + expect: { + var tokenCodes = { + get instanceof(){ + return test0; + }, + set instanceof(value){ + test0 = value; + }, + set typeof(value){ + test1 = value; + }, + get typeof(){ + return test1; + }, + set else(value){ + test2 = value; + }, + get else(){ + return test2; + } + }; + } +} \ No newline at end of file diff --git a/test/mocha/getter-setter.js b/test/mocha/getter-setter.js new file mode 100644 index 00000000..641a2026 --- /dev/null +++ b/test/mocha/getter-setter.js @@ -0,0 +1,89 @@ +var UglifyJS = require('../../'); +var assert = require("assert"); + +describe("Getters and setters", function() { + it("Should not accept operator symbols as getter/setter name", function() { + var illegalOperators = [ + "++", + "--", + "+", + "-", + "!", + "~", + "&", + "|", + "^", + "*", + "/", + "%", + ">>", + "<<", + ">>>", + "<", + ">", + "<=", + ">=", + "==", + "===", + "!=", + "!==", + "?", + "=", + "+=", + "-=", + "/=", + "*=", + "%=", + ">>=", + "<<=", + ">>>=", + "|=", + "^=", + "&=", + "&&", + "||" + ]; + var generator = function() { + var results = []; + + for (var i in illegalOperators) { + results.push({ + code: "var obj = { get " + illegalOperators[i] + "() { return test; }};", + operator: illegalOperators[i], + method: "get" + }); + results.push({ + code: "var obj = { set " + illegalOperators[i] + "(value) { test = value}};", + operator: illegalOperators[i], + method: "set" + }); + } + + return results; + }; + + var testCase = function(data) { + return function() { + UglifyJS.parse(data.code); + }; + }; + + var fail = function(data) { + return function (e) { + return e instanceof UglifyJS.JS_Parse_Error && + e.message === "Invalid getter/setter name: " + data.operator; + }; + }; + + var errorMessage = function(data) { + return "Expected but didn't get a syntax error while parsing following line:\n" + data.code; + }; + + var tests = generator(); + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + assert.throws(testCase(test), fail(test), errorMessage(test)); + } + }); + +}); From 8b71c6559b0e1773bb3455c68701ff512fc18277 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Tue, 19 Jan 2016 13:12:32 -0600 Subject: [PATCH 11/51] Mark vars with /** @const */ pragma as consts so they can be eliminated. Fixes older browser support for consts and allows more flexibility in dead code removal. --- lib/scope.js | 12 +++++++++- test/compress/dead-code.js | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index 5e93020f..22fb150d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -154,7 +154,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); - def.constant = node instanceof AST_SymbolConst; + def.constant = node instanceof AST_SymbolConst || node.has_const_pragma(); def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -357,6 +357,16 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); +AST_Symbol.DEFMETHOD("has_const_pragma", function() { + var token = this.scope.body[0] && this.scope.body[0].start; + var comments = token && token.comments_before; + if (comments && comments.length > 0) { + var last = comments[comments.length - 1]; + return /@const/.test(last.value); + } + return false; +}) + AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { except : [], diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 5009ae1e..f79b04de 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -87,3 +87,49 @@ dead_code_constant_boolean_should_warn_more: { var moo; } } + +dead_code_const_declaration: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + const CONST_FOO = false; + if (CONST_FOO) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + const CONST_FOO = !1; + var moo; + function bar() {} + } +} + +dead_code_const_annotation: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + /** @const*/ var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + } + expect: { + var CONST_FOO_ANN = !1; + var moo; + function bar() {} + } +} From 918c17bd88647899be7fa1d9adabfe635cd6102d Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Tue, 19 Jan 2016 13:24:36 -0600 Subject: [PATCH 12/51] Update README for /** @const */ --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67324dbd..6dea439a 100644 --- a/README.md +++ b/README.md @@ -395,6 +395,8 @@ separate file and include it into the build. For example you can have a ```javascript const DEBUG = false; const PRODUCTION = true; +// Alternative for environments that don't support `const` +/** @const */ var STAGING = false; // etc. ``` @@ -404,8 +406,8 @@ and build your code like this: UglifyJS will notice the constants and, since they cannot be altered, it will evaluate references to them to the value itself and drop unreachable -code as usual. The possible downside of this approach is that the build -will contain the `const` declarations. +code as usual. The build will contain the `const` declarations if you use +them. If you are targeting < ES6 environments, use `/** @const */ var`. ## Beautifier options From f97da4294a7e9adbd560ecafd94ec697de35affc Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Wed, 20 Jan 2016 10:52:48 -0600 Subject: [PATCH 13/51] Use TreeWalker for more accurate @const results and update tests --- lib/scope.js | 32 +++++++++++++++++++------ test/compress/dead-code.js | 49 +++++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 22fb150d..144ae48e 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -358,13 +358,31 @@ AST_Symbol.DEFMETHOD("global", function(){ }); AST_Symbol.DEFMETHOD("has_const_pragma", function() { - var token = this.scope.body[0] && this.scope.body[0].start; - var comments = token && token.comments_before; - if (comments && comments.length > 0) { - var last = comments[comments.length - 1]; - return /@const/.test(last.value); - } - return false; + var symbol = this; + var symbol_has_pragma = false; + var pragma_found = false; + var found_symbol = false; + // Walk the current scope, looking for a comment with the @const pragma. + // If it exists, mark a bool that will remain true only for the next iteration. + // If the next iteration is this symbol, then we return true. + // Otherwise we stop descending and get out of here. + var tw = new TreeWalker(function(node, descend){ + // This is our symbol. Was the pragma before this? + if (node.name === symbol) { + found_symbol = true; + symbol_has_pragma = pragma_found; + } + + // Look for the /** @const */ pragma + var comments_before = node.start && node.start.comments_before; + var lastComment = comments_before && comments_before[comments_before.length - 1]; + pragma_found = lastComment && /@const/.test(lastComment.value); + + // no need to descend after finding our node + return found_symbol; + }); + this.scope.walk(tw); + return symbol_has_pragma; }) AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index f79b04de..8aad336c 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -97,6 +97,7 @@ dead_code_const_declaration: { evaluate : true }; input: { + var unused; const CONST_FOO = false; if (CONST_FOO) { console.log("unreachable"); @@ -105,6 +106,7 @@ dead_code_const_declaration: { } } expect: { + var unused; const CONST_FOO = !1; var moo; function bar() {} @@ -120,7 +122,8 @@ dead_code_const_annotation: { evaluate : true }; input: { - /** @const*/ var CONST_FOO_ANN = false; + var unused; + /** @const */ var CONST_FOO_ANN = false; if (CONST_FOO_ANN) { console.log("unreachable"); var moo; @@ -128,8 +131,52 @@ dead_code_const_annotation: { } } expect: { + var unused; var CONST_FOO_ANN = !1; var moo; function bar() {} } } + +dead_code_const_annotation_complex_scope: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused_var; + /** @const */ var test = 'test'; + /** @const */ var CONST_FOO_ANN = false; + var unused_var_2; + if (CONST_FOO_ANN) { + console.log("unreachable"); + var moo; + function bar() {} + } + if (test === 'test') { + var beef = 'good'; + /** @const */ var meat = 'beef'; + var pork = 'bad'; + if (meat === 'pork') { + console.log('also unreachable'); + } else if (pork === 'good') { + console.log('reached, not const'); + } + } + } + expect: { + var unused_var; + var test = 'test'; + var CONST_FOO_ANN = !1; + var unused_var_2; + var moo; + function bar() {} + var beef = 'good'; + var meat = 'beef'; + var pork = 'bad'; + 'good' === pork && console.log('reached, not const'); + } +} From 4a7179ff9183cd27d036043c0bbcb01d1604d824 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Wed, 20 Jan 2016 11:03:41 -0600 Subject: [PATCH 14/51] Simplify by skipping extra tree walk. --- lib/scope.js | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 144ae48e..794254d4 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -94,6 +94,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var labels = new Dictionary(); var defun = null; + var last_var_had_const_pragma = false; var nesting = 0; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { @@ -151,10 +152,13 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // later. (node.scope = defun.parent_scope).def_function(node); } + else if (node instanceof AST_Var) { + last_var_had_const_pragma = node.has_const_pragma(); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); - def.constant = node instanceof AST_SymbolConst || node.has_const_pragma(); + def.constant = node instanceof AST_SymbolConst || last_var_had_const_pragma; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -357,33 +361,11 @@ AST_Symbol.DEFMETHOD("global", function(){ return this.definition().global; }); -AST_Symbol.DEFMETHOD("has_const_pragma", function() { - var symbol = this; - var symbol_has_pragma = false; - var pragma_found = false; - var found_symbol = false; - // Walk the current scope, looking for a comment with the @const pragma. - // If it exists, mark a bool that will remain true only for the next iteration. - // If the next iteration is this symbol, then we return true. - // Otherwise we stop descending and get out of here. - var tw = new TreeWalker(function(node, descend){ - // This is our symbol. Was the pragma before this? - if (node.name === symbol) { - found_symbol = true; - symbol_has_pragma = pragma_found; - } - - // Look for the /** @const */ pragma - var comments_before = node.start && node.start.comments_before; - var lastComment = comments_before && comments_before[comments_before.length - 1]; - pragma_found = lastComment && /@const/.test(lastComment.value); - - // no need to descend after finding our node - return found_symbol; - }); - this.scope.walk(tw); - return symbol_has_pragma; -}) +AST_Var.DEFMETHOD("has_const_pragma", function() { + var comments_before = this.start && this.start.comments_before; + var lastComment = comments_before && comments_before[comments_before.length - 1]; + return lastComment && /@const/.test(lastComment.value); +}); AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { From 1b703349cf824020c4dc64a58aa6d0dc3b809cea Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Wed, 20 Jan 2016 11:35:45 -0600 Subject: [PATCH 15/51] Tighten up @const regex. --- lib/scope.js | 2 +- test/compress/dead-code.js | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 794254d4..4cea5176 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -364,7 +364,7 @@ AST_Symbol.DEFMETHOD("global", function(){ AST_Var.DEFMETHOD("has_const_pragma", function() { var comments_before = this.start && this.start.comments_before; var lastComment = comments_before && comments_before[comments_before.length - 1]; - return lastComment && /@const/.test(lastComment.value); + return lastComment && /@const\b/.test(lastComment.value); }); AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ diff --git a/test/compress/dead-code.js b/test/compress/dead-code.js index 8aad336c..fa4b37d6 100644 --- a/test/compress/dead-code.js +++ b/test/compress/dead-code.js @@ -138,6 +138,29 @@ dead_code_const_annotation: { } } +dead_code_const_annotation_regex: { + options = { + dead_code : true, + loops : true, + booleans : true, + conditionals : true, + evaluate : true + }; + input: { + var unused; + // @constraint this shouldn't be a constant + var CONST_FOO_ANN = false; + if (CONST_FOO_ANN) { + console.log("reachable"); + } + } + expect: { + var unused; + var CONST_FOO_ANN = !1; + CONST_FOO_ANN && console.log('reachable'); + } +} + dead_code_const_annotation_complex_scope: { options = { dead_code : true, @@ -149,7 +172,8 @@ dead_code_const_annotation_complex_scope: { input: { var unused_var; /** @const */ var test = 'test'; - /** @const */ var CONST_FOO_ANN = false; + // @const + var CONST_FOO_ANN = false; var unused_var_2; if (CONST_FOO_ANN) { console.log("unreachable"); From 799509e145e56a9a6ccbaad6d32e1c404e0469eb Mon Sep 17 00:00:00 2001 From: Jeremy Marzka Date: Sun, 17 Jan 2016 21:54:09 -0500 Subject: [PATCH 16/51] Added a mangle properties option --- README.md | 3 +++ tools/node.js | 31 ++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6dea439a..9cde9ee8 100644 --- a/README.md +++ b/README.md @@ -626,6 +626,9 @@ Other options: - `mangle` — pass `false` to skip mangling names. +- `mangleProperties` (default `false`) — pass an object to specify custom + mangle property options. + - `output` (default `null`) — pass an object if you wish to specify additional [output options][codegen]. The defaults are optimized for best compression. diff --git a/tools/node.js b/tools/node.js index f6048661..5764286e 100644 --- a/tools/node.js +++ b/tools/node.js @@ -32,15 +32,17 @@ UglifyJS.AST_Node.warn_function = function(txt) { exports.minify = function(files, options) { options = UglifyJS.defaults(options, { - spidermonkey : false, - outSourceMap : null, - sourceRoot : null, - inSourceMap : null, - fromString : false, - warnings : false, - mangle : {}, - output : null, - compress : {} + spidermonkey : false, + outSourceMap : null, + sourceRoot : null, + inSourceMap : null, + fromString : false, + warnings : false, + mangle : {}, + mangleProperties : false, + nameCache : null, + output : null, + compress : {} }); UglifyJS.base54.reset(); @@ -77,14 +79,21 @@ exports.minify = function(files, options) { toplevel = toplevel.transform(sq); } - // 3. mangle + // 3. mangle properties + if (options.mangleProperties || options.nameCache) { + options.mangleProperties.cache = UglifyJS.readNameCache(options.nameCache, "props"); + toplevel = UglifyJS.mangle_properties(toplevel, options.mangleProperties); + UglifyJS.writeNameCache(options.nameCache, "props", options.mangleProperties.cache); + } + + // 4. mangle if (options.mangle) { toplevel.figure_out_scope(options.mangle); toplevel.compute_char_frequency(options.mangle); toplevel.mangle_names(options.mangle); } - // 4. output + // 5. output var inMap = options.inSourceMap; var output = {}; if (typeof options.inSourceMap == "string") { From 915f907186bf4029c830716b9d018e2d09ad4120 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Wed, 27 Jan 2016 11:36:03 +0200 Subject: [PATCH 17/51] Add start/end in the `arguments` definition (keeps my https://github.com/mishoo/jsinfo.el working) --- lib/scope.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index 4cea5176..20d9d730 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -242,7 +242,7 @@ AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; - var symbol = new AST_VarDef({ name: "arguments" }); + var symbol = new AST_VarDef({ name: "arguments", start: this.start, end: this.end }); var def = new SymbolDef(this, this.variables.size(), symbol); this.variables.set(symbol.name, def); }); From f4c2ea37bf9231b6f76804e74ee157be916280de Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 27 Jan 2016 02:17:06 -0500 Subject: [PATCH 18/51] Collapse single use var definitions Fix #721 --- lib/compress.js | 146 +++++ lib/transform.js | 2 +- test/compress/collapse_vars.js | 1047 ++++++++++++++++++++++++++++++++ 3 files changed, 1194 insertions(+), 1 deletion(-) create mode 100644 test/compress/collapse_vars.js diff --git a/lib/compress.js b/lib/compress.js index 1f5988f7..814e9a8b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -66,6 +66,7 @@ function Compressor(options, false_by_default) { hoist_vars : false, if_return : !false_by_default, join_vars : !false_by_default, + collapse_vars : false, cascade : !false_by_default, side_effects : !false_by_default, pure_getters : false, @@ -218,6 +219,9 @@ merge(Compressor.prototype, { if (compressor.option("join_vars")) { statements = join_consecutive_vars(statements, compressor); } + if (compressor.option("collapse_vars")) { + statements = collapse_single_use_vars(statements, compressor); + } } while (CHANGED && max_iter-- > 0); if (compressor.option("negate_iife")) { @@ -226,6 +230,148 @@ merge(Compressor.prototype, { return statements; + function collapse_single_use_vars(statements, compressor) { + // Iterate statements backwards looking for a statement with a var/const + // declaration immediately preceding it. Grab the rightmost var definition + // and if it has exactly one reference then attempt to replace its reference + // in the statement with the var value and then erase the var definition. + + var self = compressor.self(); + var var_defs_removed = false; + for (var stat_index = statements.length; --stat_index >= 0;) { + var stat = statements[stat_index]; + if (stat instanceof AST_Definitions) continue; + + // Process child blocks of statement if present. + [stat, stat.body, stat.alternative, stat.bcatch, stat.bfinally].forEach(function(node) { + node && node.body && collapse_single_use_vars(node.body, compressor); + }); + + // The variable definition must precede a statement. + if (stat_index <= 0) break; + var prev_stat_index = stat_index - 1; + var prev_stat = statements[prev_stat_index]; + if (!(prev_stat instanceof AST_Definitions)) continue; + var var_defs = prev_stat.definitions; + if (var_defs == null) continue; + + // Scan variable definitions from right to left. + var side_effects_encountered = false; + var lvalues_encountered = false; + var lvalues = {}; + for (var var_defs_index = var_defs.length; --var_defs_index >= 0;) { + var var_decl = var_defs[var_defs_index]; + if (var_decl.value == null) continue; + + // Only interested in cases with just one reference to the variable. + var var_name = var_decl.name.name; + var def = self.find_variable && self.find_variable(var_name); + if (!def || !def.references || def.references.length !== 1 || var_name == "arguments") { + side_effects_encountered = true; + continue; + } + var ref = def.references[0]; + + // Don't replace ref if eval() or with statement in scope. + if (ref.scope.uses_eval || ref.scope.uses_with) break; + + // Constant single use vars can be replaced in any scope. + if (var_decl.value.is_constant(compressor)) { + var ctt = new TreeTransformer(function(node) { + if (node === ref) + return replace_var(node, ctt.parent(), true); + }); + stat.transform(ctt); + continue; + } + + // Restrict var replacement to constants if side effects encountered. + if (side_effects_encountered |= lvalues_encountered) continue; + + // Non-constant single use vars can only be replaced in same scope. + if (ref.scope !== self) { + side_effects_encountered |= var_decl.value.has_side_effects(compressor); + continue; + } + + // Detect lvalues in var value. + var tw = new TreeWalker(function(node){ + if (node instanceof AST_SymbolRef && is_lvalue(node, tw.parent())) { + lvalues[node.name] = lvalues_encountered = true; + } + }); + var_decl.value.walk(tw); + + // Replace the non-constant single use var in statement if side effect free. + var unwind = false; + var tt = new TreeTransformer( + function preorder(node) { + if (unwind) return node; + var parent = tt.parent(); + if (node instanceof AST_Lambda + || node instanceof AST_Try + || node instanceof AST_With + || node instanceof AST_IterationStatement + || (parent instanceof AST_Switch && node !== parent.expression)) { + return unwind = true, node; + } + }, + function postorder(node) { + if (unwind) return node; + if (node === ref) + return unwind = true, replace_var(node, tt.parent(), false); + if (side_effects_encountered |= node.has_side_effects(compressor)) + return unwind = true, node; + if (lvalues_encountered && node instanceof AST_SymbolRef && node.name in lvalues) { + side_effects_encountered = true; + return unwind = true, node; + } + } + ); + stat.transform(tt); + } + } + + // Remove extraneous empty statments in block after removing var definitions. + // Leave at least one statement in `statements`. + if (var_defs_removed) for (var i = statements.length; --i >= 0;) { + if (statements.length > 1 && statements[i] instanceof AST_EmptyStatement) + statements.splice(i, 1); + } + + return statements; + + function is_lvalue(node, parent) { + return node instanceof AST_SymbolRef && ( + (parent instanceof AST_Assign && node === parent.left) + || (parent instanceof AST_Unary && parent.expression === node + && (parent.operator == "++" || parent.operator == "--"))); + } + function replace_var(node, parent, is_constant) { + if (is_lvalue(node, parent)) return node; + + // Remove var definition and return its value to the TreeTransformer to replace. + var value = var_decl.value; + var_decl.value = null; + + var_defs.splice(var_defs_index, 1); + if (var_defs.length === 0) { + statements[prev_stat_index] = make_node(AST_EmptyStatement, self); + var_defs_removed = true; + } + // Further optimize statement after substitution. + stat.walk(new TreeWalker(function(node){ + delete node._squeezed; + delete node._optimized; + })); + + compressor.warn("Replacing " + (is_constant ? "constant" : "variable") + + " " + var_name + " [{file}:{line},{col}]", node.start); + CHANGED = true; + return value; + } + } + function process_for_angular(statements) { function has_inject(comment) { return /@ngInject/.test(comment.value); diff --git a/lib/transform.js b/lib/transform.js index 62e6e02b..3018e8ff 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -64,7 +64,7 @@ TreeTransformer.prototype = new TreeWalker; x = this; descend(x, tw); } else { - tw.stack[tw.stack.length - 1] = x = this.clone(); + tw.stack[tw.stack.length - 1] = x = this; descend(x, tw); y = tw.after(x, in_list); if (y !== undefined) x = y; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js new file mode 100644 index 00000000..e0235972 --- /dev/null +++ b/test/compress/collapse_vars.js @@ -0,0 +1,1047 @@ +collapse_vars_side_effects_1: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + var e = 7; + var s = "abcdef"; + var i = 2; + var log = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, y, z, e); + } + function f2() { + var e = 7; + var log = console.log.bind(console); + var s = "abcdef"; + var i = 2; + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, i, y, z, e); + } + function f3() { + var e = 7; + var s = "abcdef"; + var i = 2; + var log = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + log(x, z, y, e); + } + function f4() { + var log = console.log.bind(console), + i = 10, + x = i += 2, + y = i += 3, + z = i += 4; + log(x, z, y, i); + } + } + expect: { + function f1() { + var s = "abcdef", i = 2; + console.log.bind(console)(s.charAt(i++), s.charAt(i++), s.charAt(i++), 7); + } + function f2() { + var log = console.log.bind(console), + s = "abcdef", + i = 2, + x = s.charAt(i++), + y = s.charAt(i++), + z = s.charAt(i++); + log(x, i, y, z, 7); + } + function f3() { + var s = "abcdef", + i = 2, + log = console.log.bind(console), + x = s.charAt(i++), + y = s.charAt(i++); + log(x, s.charAt(i++), y, 7); + } + function f4() { + var log = console.log.bind(console), + i = 10, + x = i += 2, + y = i += 3; + log(x, i += 4, y, i); + } + } +} + +collapse_vars_side_effects_2: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function fn(x) { return console.log(x), x; } + + function p1() { var a = foo(), b = bar(), c = baz(); return a + b + c; } + function p2() { var a = foo(), c = bar(), b = baz(); return a + b + c; } + function p3() { var b = foo(), a = bar(), c = baz(); return a + b + c; } + function p4() { var b = foo(), c = bar(), a = baz(); return a + b + c; } + function p5() { var c = foo(), a = bar(), b = baz(); return a + b + c; } + function p6() { var c = foo(), b = bar(), a = baz(); return a + b + c; } + + function q1() { var a = foo(), b = bar(), c = baz(); return fn(a + b + c); } + function q2() { var a = foo(), c = bar(), b = baz(); return fn(a + b + c); } + function q3() { var b = foo(), a = bar(), c = baz(); return fn(a + b + c); } + function q4() { var b = foo(), c = bar(), a = baz(); return fn(a + b + c); } + function q5() { var c = foo(), a = bar(), b = baz(); return fn(a + b + c); } + function q6() { var c = foo(), b = bar(), a = baz(); return fn(a + b + c); } + + function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r4() { var b = foo(), c = bar(), a = baz(); return fn(a) + fn(b) + fn(c); } + function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r6() { var c = foo(), b = bar(), a = baz(); return fn(a) + fn(b) + fn(c); } + + function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); } + function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); } + + function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); } + function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); } + } + expect: { + function fn(x) { return console.log(x), x; } + + function p1() { return foo() + bar() + baz(); } + function p2() { var a = foo(), c = bar(); return a + baz() + c; } + function p3() { var b = foo(); return bar() + b + baz(); } + function p4() { var b = foo(), c = bar(); return baz() + b + c; } + function p5() { var c = foo(); return bar() + baz() + c; } + function p6() { var c = foo(), b = bar(); return baz() + b + c; } + + function q1() { return fn(foo() + bar() + baz()); } + function q2() { var a = foo(), c = bar(); return fn(a + baz() + c); } + function q3() { var b = foo(); return fn(bar() + b + baz()); } + function q4() { var b = foo(), c = bar(); return fn(baz() + b + c); } + function q5() { var c = foo(); return fn(bar() + baz() + c); } + function q6() { var c = foo(), b = bar(); return fn(baz() + b + c); } + + function r1() { var a = foo(), b = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r2() { var a = foo(), c = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r3() { var b = foo(), a = bar(), c = baz(); return fn(a) + fn(b) + fn(c); } + function r4() { var b = foo(), c = bar(); return fn(baz()) + fn(b) + fn(c); } + function r5() { var c = foo(), a = bar(), b = baz(); return fn(a) + fn(b) + fn(c); } + function r6() { var c = foo(), b = bar(); return fn(baz()) + fn(b) + fn(c); } + + function s1() { var a = foo(), b = bar(), c = baz(); return g(a + b + c); } + function s6() { var c = foo(), b = bar(), a = baz(); return g(a + b + c); } + + function t1() { var a = foo(), b = bar(), c = baz(); return g(a) + g(b) + g(c); } + function t6() { var c = foo(), b = bar(), a = baz(); return g(a) + g(b) + g(c); } + } +} + +collapse_vars_issue_721: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + define(["require", "exports", 'handlebars'], function (require, exports, hb) { + var win = window; + var _hb = win.Handlebars = hb; + return _hb; + }); + def(function (hb) { + var win = window; + var prop = 'Handlebars'; + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = 'Handlebars'; + var win = window; + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = 'Handlebars'; + var win = g(); + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var prop = g1(); + var win = g2(); + var _hb = win[prop] = hb; + return _hb; + }); + def(function (hb) { + var win = g2(); + var prop = g1(); + var _hb = win[prop] = hb; + return _hb; + }); + } + expect: { + define([ "require", "exports", "handlebars" ], function(require, exports, hb) { + return window.Handlebars = hb; + }), + def(function(hb) { + return window.Handlebars = hb; + }), + def(function(hb) { + return window.Handlebars = hb; + }), + def(function (hb) { + return g().Handlebars = hb; + }), + def(function (hb) { + var prop = g1(); + return g2()[prop] = hb; + }), + def(function (hb) { + return g2()[g1()] = hb; + }); + } +} + +collapse_vars_properties: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(obj) { + var prop = 'LiteralProperty'; + return !!-+obj[prop]; + } + function f2(obj) { + var prop1 = 'One'; + var prop2 = 'Two'; + return ~!!-+obj[prop1 + prop2]; + } + } + expect: { + function f1(obj) { + return !!-+obj.LiteralProperty; + } + function f2(obj) { + return ~!!-+obj.OneTwo; + } + } +} + +collapse_vars_if: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + var not_used = sideeffect(), x = g1 + g2; + var y = x / 4, z = 'Bar' + y; + if ('x' != z) { return g9; } + else return g5; + } + function f2() { + var x = g1 + g2, not_used = sideeffect(); + var y = x / 4 + var z = 'Bar' + y; + if ('x' != z) { return g9; } + else return g5; + } + function f3(x) { + if (x) { + var a = 1; + return a; + } + else { + var b = 2; + return b; + } + } + } + expect: { + function f1() { + sideeffect(); + return "x" != "Bar" + (g1 + g2) / 4 ? g9 : g5; + } + function f2() { + var x = g1 + g2; + sideeffect(); + return "x" != "Bar" + x / 4 ? g9 : g5; + } + function f3(x) { + if (x) { + return 1; + } + return 2; + } + } +} + +collapse_vars_while: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:false, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(y) { + // Neither the non-constant while condition `c` will be + // replaced, nor the non-constant `x` in the body. + var x = y, c = 3 - y; + while (c) { return x; } + var z = y * y; + return z; + } + function f2(y) { + // The constant `x` will be replaced in the while body. + var x = 7; + while (y) { return x; } + var z = y * y; + return z; + } + function f3(y) { + // The non-constant `n` will not be replaced in the while body. + var n = 5 - y; + while (y) { return n; } + var z = y * y; + return z; + } + } + expect: { + function f1(y) { + var x = y, c = 3 - y; + while (c) return x; + return y * y; + } + function f2(y) { + while (y) return 7; + return y * y + } + function f3(y) { + var n = 5 - y; + while (y) return n; + return y * y; + } + } +} + +collapse_vars_do_while: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:false, loops:false, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(y) { + // The constant do-while condition `c` will be replaced. + var c = 9; + do { } while (c === 77); + } + function f2(y) { + // The non-constant do-while condition `c` will not be replaced. + var c = 5 - y; + do { } while (c); + } + function f3(y) { + // The constant `x` will be replaced in the do loop body. + function fn(n) { console.log(n); } + var a = 2, x = 7; + do { + fn(a = x); + break; + } while (y); + } + function f4(y) { + // The non-constant `a` will not be replaced in the do loop body. + var a = y / 4; + do { + return a; + } while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + // The non-constant `a` will be replaced in p(a) + // because it is declared in same block. + var a = y - 3; + p(a); + } while (--y); + } + } + expect: { + function f1(y) { + do ; while (false); + } + function f2(y) { + var c = 5 - y; + do ; while (c); + } + function f3(y) { + function fn(n) { console.log(n); } + var a = 2; + do { + fn(a = 7); + break; + } while (y); + } + function f4(y) { + var a = y / 4; + do + return a; + while (y); + } + function f5(y) { + function p(x) { console.log(x); } + do { + p(y - 3); + } while (--y); + } + } +} + +collapse_vars_seq: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + var f1 = function(x, y) { + var a, b, r = x + y, q = r * r, z = q - r; + a = z, b = 7; + return a + b; + }; + } + expect: { + var f1 = function(x, y) { + var a, b, r = x + y; + return a = r * r - r, b = 7, a + b + }; + } +} + +collapse_vars_throw: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + var f1 = function(x, y) { + var a, b, r = x + y, q = r * r, z = q - r; + a = z, b = 7; + throw a + b; + }; + } + expect: { + var f1 = function(x, y) { + var a, b, r = x + y; + throw a = r * r - r, b = 7, a + b + }; + } +} + +collapse_vars_switch: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + var not_used = sideeffect(), x = g1 + g2; + var y = x / 4, z = 'Bar' + y; + switch (z) { case 0: return g9; } + } + function f2() { + var x = g1 + g2, not_used = sideeffect(); + var y = x / 4 + var z = 'Bar' + y; + switch (z) { case 0: return g9; } + } + function f3(x) { + switch(x) { case 1: var a = 3 - x; return a; } + } + } + expect: { + function f1() { + sideeffect(); + switch ("Bar" + (g1 + g2) / 4) { case 0: return g9 } + } + function f2() { + var x = g1 + g2; + sideeffect(); + switch ("Bar" + x / 4) { case 0: return g9 } + } + function f3(x) { + // verify no extraneous semicolon in case block before return + // when the var definition was eliminated + switch(x) { case 1: return 3 - x; } + } + } +} + +collapse_vars_assignment: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function log(x) { return console.log(x), x; } + function f0(c) { + var a = 3 / c; + return a = a; + } + function f1(c) { + const a = 3 / c; + const b = 1 - a; + return b; + } + function f2(c) { + var a = 3 / c; + var b = a - 7; + return log(c = b); + } + function f3(c) { + var a = 3 / c; + var b = a - 7; + return log(c |= b); + } + function f4(c) { + var a = 3 / c; + var b = 2; + return log(b += a); + } + function f5(c) { + var b = 2; + var a = 3 / c; + return log(b += a); + } + function f6(c) { + var b = g(); + var a = 3 / c; + return log(b += a); + } + } + expect: { + function log(x) { return console.log(x), x; } + function f0(c) { + var a = 3 / c; + return a = a; + } + function f1(c) { + return 1 - 3 / c + } + function f2(c) { + return log(c = 3 / c - 7); + } + function f3(c) { + return log(c |= 3 / c - 7); + } + function f4(c) { + var b = 2; + return log(b += 3 / c); + } + function f5(c) { + var b = 2; + return log(b += 3 / c); + } + function f6(c) { + var b = g(); + return log(b += 3 / c); + } + } +} + +collapse_vars_lvalues: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + function f3(x) { var a = (x -= 3), b = x + a; return b; } + function f4(x) { var a = (x -= 3); return x + a; } + function f5(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return b - c; } + function f6(x) { var w = e1(), v = e2(), c = v = --x, b = w = x; return c - b; } + function f7(x) { var w = e1(), v = e2(), c = v - x, b = w = x; return b - c; } + function f8(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return b - c; } + function f9(x) { var w = e1(), v = e2(), b = w = x, c = v - x; return c - b; } + } + expect: { + function f0(x) { var i = ++x; return x += i; } + function f1(x) { var a = (x -= 3); return x += a; } + function f2(x) { var z = x, a = ++z; return z += a; } + function f3(x) { var a = (x -= 3); return x + a; } + function f4(x) { var a = (x -= 3); return x + a; } + function f5(x) { var w = e1(), v = e2(), c = v = --x; return (w = x) - c; } + function f6(x) { var w = e1(), v = e2(); return (v = --x) - (w = x); } + function f7(x) { var w = e1(), v = e2(), c = v - x; return (w = x) - c; } + function f8(x) { var w = e1(), v = e2(); return (w = x) - (v - x); } + function f9(x) { var w = e1(); return e2() - x - (w = x); } + + } +} + +collapse_vars_misc1: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(o, a, h) { + var b = 3 - a; + var obj = o; + var seven = 7; + var prop = 'run'; + var t = obj[prop](b)[seven] = h; + return t; + } + function f1(x) { var y = 5 - x; return y; } + function f2(x) { const z = foo(), y = z / (5 - x); return y; } + function f3(x) { var z = foo(), y = (5 - x) / z; return y; } + function f4(x) { var z = foo(), y = (5 - u) / z; return y; } + function f5(x) { const z = foo(), y = (5 - window.x) / z; return y; } + function f6() { var b = window.a * window.z; return b && zap(); } + function f7() { var b = window.a * window.z; return b + b; } + function f8() { var b = window.a * window.z; var c = b + 5; return b + c; } + function f9() { var b = window.a * window.z; return bar() || b; } + function f10(x) { var a = 5, b = 3; return a += b; } + function f11(x) { var a = 5, b = 3; return a += --b; } + } + expect: { + function f0(o, a, h) { + var b = 3 - a; + return o.run(b)[7] = h; + } + function f1(x) { return 5 - x } + function f2(x) { return foo() / (5 - x) } + function f3(x) { return (5 - x) / foo() } + function f4(x) { var z = foo(); return (5 - u) / z } + function f5(x) { const z = foo(); return (5 - window.x) / z } + function f6() { return window.a * window.z && zap() } + function f7() { var b = window.a * window.z; return b + b } + function f8() { var b = window.a * window.z; return b + (b + 5) } + function f9() { var b = window.a * window.z; return bar() || b } + function f10(x) { var a = 5; return a += 3; } + function f11(x) { var a = 5, b = 3; return a += --b; } + } +} + +collapse_vars_self_reference: { + options = { + collapse_vars:true, unused:false, + sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + // avoid bug in self-referential declaration. + function f1() { + var self = { + inner: function() { return self; } + }; + } + function f2() { + var self = { inner: self }; + } + } + expect: { + // note: `unused` option is false + function f1() { + var self = { + inner: function() { return self } + }; + } + function f2() { + var self = { inner: self }; + } + } +} + +collapse_vars_repeated: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + var dummy = 3, a = 5, unused = 2, a = 1, a = 3; + return -a; + } + function f2() { + var a = 3, a = a + 2; + return a; + } + } + expect: { + function f1() { + return -3 + } + function f2() { + var a = 3, a = a + 2; + return a + } + } +} + +collapse_vars_closures: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function constant_vars_can_be_replaced_in_any_scope() { + var outer = 3; + return function() { return outer; } + } + function non_constant_vars_can_only_be_replace_in_same_scope(x) { + var outer = x; + return function() { return outer; } + } + } + expect: { + function constant_vars_can_be_replaced_in_any_scope() { + return function() { return 3 } + } + function non_constant_vars_can_only_be_replace_in_same_scope(x) { + var outer = x + return function() { return outer } + } + } +} + +collapse_vars_unary: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(o, p) { + var x = o[p]; + delete x; + } + function f1(n) { + var k = !!n; + return n > +k; + } + function f2(n) { + // test unary with constant + var k = 7; + return k--; + } + function f3(n) { + // test unary with constant + var k = 7; + return ++k; + } + function f4(n) { + // test unary with non-constant + var k = 8 - n; + return k--; + } + function f5(n) { + // test unary with non-constant + var k = 9 - n; + return ++k; + } + } + expect: { + function f0(o, p) { + delete o[p]; + } + function f1(n) { + return n > +!!n + } + function f2(n) { + var k = 7; + return k-- + } + function f3(n) { + var k = 7; + return ++k + } + function f4(n) { + var k = 8 - n; + return k--; + } + function f5(n) { + var k = 9 - n; + return ++k; + } + } +} + +collapse_vars_try: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + try { + var a = 1; + return a; + } + catch (ex) { + var b = 2; + return b; + } + finally { + var c = 3; + return c; + } + } + function f2() { + var t = could_throw(); // shouldn't be replaced in try block + try { + return t + might_throw(); + } + catch (ex) { + return 3; + } + } + } + expect: { + function f1() { + try { + return 1; + } + catch (ex) { + return 2; + } + finally { + return 3; + } + } + function f2() { + var t = could_throw(); + try { + return t + might_throw(); + } + catch (ex) { + return 3; + } + } + } +} + +collapse_vars_array: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(x, y) { + var z = x + y; + return [z]; + } + function f2(x, y) { + var z = x + y; + return [x, side_effect(), z]; + } + function f3(x, y) { + var z = f(x + y); + return [ [3], [z, x, y], [g()] ]; + } + } + expect: { + function f1(x, y) { + return [x + y] + } + function f2(x, y) { + var z = x + y + return [x, side_effect(), z] + } + function f3(x, y) { + return [ [3], [f(x + y), x, y], [g()] ] + } + } +} + +collapse_vars_object: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(x, y) { + var z = x + y; + return { + get b() { return 7; }, + r: z + }; + } + function f1(x, y) { + var z = x + y; + return { + r: z, + get b() { return 7; } + }; + } + function f2(x, y) { + var z = x + y; + var k = x - y; + return { + q: k, + r: g(x), + s: z + }; + } + function f3(x, y) { + var z = f(x + y); + return [{ + a: {q: x, r: y, s: z}, + b: g() + }]; + } + } + expect: { + function f0(x, y) { + var z = x + y; + return { + get b() { return 7; }, + r: z + }; + } + function f1(x, y) { + return { + r: x + y, + get b() { return 7; } + }; + } + function f2(x, y) { + var z = x + y; + return { + q: x - y, + r: g(x), + s: z + }; + } + function f3(x, y) { + return [{ + a: {q: x, r: y, s: f(x + y)}, + b: g() + }]; + } + } +} + +collapse_vars_eval_and_with: { + options = { + collapse_vars:true, sequences:false, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + // Don't attempt to collapse vars in presence of eval() or with statement. + (function f0() { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(); + (function f1() { + var o = {a: 1}, a = 2; + with (o) console.log(a); + })(); + (function f2() { + var o = {a: 1}, a = 2; + return function() { with (o) console.log(a) }; + })()(); + } + expect: { + (function f0() { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(); + (function f1() { + var o = {a: 1}, a = 2; + with(o) console.log(a); + })(); + (function f2() { + var o = {a: 1}, a = 2; + return function() { with (o) console.log(a) }; + })()(); + } +} + +collapse_vars_constants: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1(x) { + var a = 4, b = x.prop, c = 5, d = sideeffect1(), e = sideeffect2(); + return b + (function() { return d - a * e - c; })(); + } + function f2(x) { + var a = 4, b = x.prop, c = 5, not_used = sideeffect1(), e = sideeffect2(); + return b + (function() { return -a * e - c; })(); + } + function f3(x) { + var a = 4, b = x.prop, c = 5, not_used = sideeffect1(); + return b + (function() { return -a - c; })(); + } + } + expect: { + function f1(x) { + var b = x.prop, d = sideeffect1(), e = sideeffect2(); + return b + (function() { return d - 4 * e - 5; })(); + } + function f2(x) { + var b = x.prop, e = (sideeffect1(), sideeffect2()); + return b + (function() { return -4 * e - 5; })(); + } + function f3(x) { + var b = x.prop; + sideeffect1(); + return b + (function() { return -9; })(); + } + } +} + +collapse_vars_arguments: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + var outer = function() { + // Do not replace `arguments` but do replace the constant `k` before it. + var k = 7, arguments = 5, inner = function() { console.log(arguments); } + inner(k, 1); + } + outer(); + } + expect: { + (function() { + (function(){console.log(arguments);})(7, 1); + })(); + } +} + From 0a38a688f9a327d677d7f8314b5c4dcf4590b798 Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 27 Jan 2016 14:18:46 -0500 Subject: [PATCH 19/51] fix bug in collapse_vars for right side of "||" and "&&" --- lib/compress.js | 5 +++- test/compress/collapse_vars.js | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 814e9a8b..6af086f7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -312,8 +312,11 @@ merge(Compressor.prototype, { || node instanceof AST_Try || node instanceof AST_With || node instanceof AST_IterationStatement + || (parent instanceof AST_Binary + && (parent.operator == "&&" || parent.operator == "||") + && node === parent.right) || (parent instanceof AST_Switch && node !== parent.expression)) { - return unwind = true, node; + return side_effects_encountered = unwind = true, node; } }, function postorder(node) { diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index e0235972..f67b3f47 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1045,3 +1045,45 @@ collapse_vars_arguments: { } } +collapse_vars_short_circuit: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f0(x) { var a = foo(), b = bar(); return b || x; } + function f1(x) { var a = foo(), b = bar(); return b && x; } + function f2(x) { var a = foo(), b = bar(); return x && a && b; } + function f3(x) { var a = foo(), b = bar(); return a && x; } + function f4(x) { var a = foo(), b = bar(); return a && x && b; } + function f5(x) { var a = foo(), b = bar(); return x || a || b; } + function f6(x) { var a = foo(), b = bar(); return a || x || b; } + function f7(x) { var a = foo(), b = bar(); return a && b && x; } + function f8(x,y) { var a = foo(), b = bar(); return (x || a) && (y || b); } + function f9(x,y) { var a = foo(), b = bar(); return (x && a) || (y && b); } + function f10(x,y) { var a = foo(), b = bar(); return (x - a) || (y - b); } + function f11(x,y) { var a = foo(), b = bar(); return (x - b) || (y - a); } + function f12(x,y) { var a = foo(), b = bar(); return (x - y) || (b - a); } + function f13(x,y) { var a = foo(), b = bar(); return (a - b) || (x - y); } + function f14(x,y) { var a = foo(), b = bar(); return (b - a) || (x - y); } + } + expect: { + function f0(x) { foo(); return bar() || x; } + function f1(x) { foo(); return bar() && x; } + function f2(x) { var a = foo(), b = bar(); return x && a && b; } + function f3(x) { var a = foo(); bar(); return a && x; } + function f4(x) { var a = foo(), b = bar(); return a && x && b; } + function f5(x) { var a = foo(), b = bar(); return x || a || b; } + function f6(x) { var a = foo(), b = bar(); return a || x || b; } + function f7(x) { var a = foo(), b = bar(); return a && b && x; } + function f8(x,y) { var a = foo(), b = bar(); return (x || a) && (y || b); } + function f9(x,y) { var a = foo(), b = bar(); return (x && a) || (y && b); } + function f10(x,y) { var a = foo(), b = bar(); return (x - a) || (y - b); } + function f11(x,y) { var a = foo(); return (x - bar()) || (y - a); } + function f12(x,y) { var a = foo(), b = bar(); return (x - y) || (b - a); } + function f13(x,y) { return (foo() - bar()) || (x - y); } + function f14(x,y) { var a = foo(); return (bar() - a) || (x - y); } + } +} + From 3eb9101918e1c8127f960624bd11f3707215b012 Mon Sep 17 00:00:00 2001 From: Bryan Rayner Date: Wed, 27 Jan 2016 14:24:32 -0600 Subject: [PATCH 20/51] Add mangleProperties documentation to README Add additional documentation to mangleProperties. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9cde9ee8..4bd1cab5 100644 --- a/README.md +++ b/README.md @@ -636,6 +636,10 @@ Other options: - `compress` (default `{}`) — pass `false` to skip compressing entirely. Pass an object to specify custom [compressor options][compressor]. +##### mangleProperties options + + - `regex` — Pass a RegExp to only mangle certain names (maps to the `--mange-regex` CLI arguments option) + We could add more options to `UglifyJS.minify` — if you need additional functionality please suggest! From af2472d85e25e2bddad0b663b38281aeb61536e9 Mon Sep 17 00:00:00 2001 From: kzc Date: Wed, 27 Jan 2016 18:35:49 -0500 Subject: [PATCH 21/51] collapse_vars: fix bug in repeated var defs of same name --- lib/compress.js | 15 ++++++++++++--- test/compress/collapse_vars.js | 26 +++++++++++++++++++++----- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 6af086f7..6cd5571e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -255,16 +255,25 @@ merge(Compressor.prototype, { var var_defs = prev_stat.definitions; if (var_defs == null) continue; - // Scan variable definitions from right to left. + var var_names_seen = {}; var side_effects_encountered = false; var lvalues_encountered = false; var lvalues = {}; + + // Scan variable definitions from right to left. for (var var_defs_index = var_defs.length; --var_defs_index >= 0;) { + + // Obtain var declaration and var name with basic sanity check. var var_decl = var_defs[var_defs_index]; - if (var_decl.value == null) continue; + if (var_decl.value == null) break; + var var_name = var_decl.name.name; + if (!var_name || !var_name.length) break; + + // Bail if we've seen a var definition of same name before. + if (var_name in var_names_seen) break; + var_names_seen[var_name] = true; // Only interested in cases with just one reference to the variable. - var var_name = var_decl.name.name; var def = self.find_variable && self.find_variable(var_name); if (!def || !def.references || def.references.length !== 1 || var_name == "arguments") { side_effects_encountered = true; diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index f67b3f47..39fee597 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -685,19 +685,35 @@ collapse_vars_repeated: { var dummy = 3, a = 5, unused = 2, a = 1, a = 3; return -a; } - function f2() { - var a = 3, a = a + 2; + function f2(x) { + var a = 3, a = x; return a; } + (function(x){ + var a = "GOOD" + x, e = "BAD", k = "!", e = a; + console.log(e + k); + })("!"), + + (function(x){ + var a = "GOOD" + x, e = "BAD" + x, k = "!", e = a; + console.log(e + k); + })("!"); } expect: { function f1() { return -3 } - function f2() { - var a = 3, a = a + 2; - return a + function f2(x) { + return x } + (function(x){ + var a = "GOOD" + x, e = "BAD", e = a; + console.log(e + "!"); + })("!"), + (function(x){ + var a = "GOOD" + x, e = "BAD" + x, e = a; + console.log(e + "!"); + })("!"); } } From 00c8d1d24149e4172ce9ddd237e4c7620a1346d3 Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 28 Jan 2016 11:01:17 -0500 Subject: [PATCH 22/51] collapse_vars: document option in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4bd1cab5..a04d0fc9 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,9 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` +- `collapse_vars` -- default `false`. Collapse single-use var and const + definitions when possible. + - `warnings` -- display warnings when dropping unreachable code or unused declarations etc. From 12e6ad326cdbe93698b54745727efb0b5e5c4216 Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 28 Jan 2016 11:04:30 -0500 Subject: [PATCH 23/51] collapse_vars: small change to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a04d0fc9..9edfa05c 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ to set `true`; it's effectively a shortcut for `foo=true`). - `cascade` -- small optimization for sequences, transform `x, x` into `x` and `x = something(), x` into `x = something()` -- `collapse_vars` -- default `false`. Collapse single-use var and const +- `collapse_vars` -- default `false`. Collapse single-use `var` and `const` definitions when possible. - `warnings` -- display warnings when dropping unreachable code or unused From 929de2b0de0429bb076f73ebcbb19df53e4d1704 Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 28 Jan 2016 12:17:06 -0500 Subject: [PATCH 24/51] collapse_vars: fix if/else and ternary operator side effects --- lib/compress.js | 2 ++ test/compress/collapse_vars.js | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 6cd5571e..68471a5e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -321,6 +321,8 @@ merge(Compressor.prototype, { || node instanceof AST_Try || node instanceof AST_With || node instanceof AST_IterationStatement + || (parent instanceof AST_If && node !== parent.condition) + || (parent instanceof AST_Conditional && node !== parent.condition) || (parent instanceof AST_Binary && (parent.operator == "&&" || parent.operator == "||") && node === parent.right) diff --git a/test/compress/collapse_vars.js b/test/compress/collapse_vars.js index 39fee597..934a5c73 100644 --- a/test/compress/collapse_vars.js +++ b/test/compress/collapse_vars.js @@ -1103,3 +1103,53 @@ collapse_vars_short_circuit: { } } +collapse_vars_short_circuited_conditions: { + options = { + collapse_vars: true, + sequences: false, + dead_code: true, + conditionals: false, + comparisons: false, + evaluate: true, + booleans: true, + loops: true, + unused: true, + hoist_funs: true, + keep_fargs: true, + if_return: false, + join_vars: true, + cascade: true, + side_effects: true, + } + input: { + function c1(x) { var a = foo(), b = bar(), c = baz(); return a ? b : c; } + function c2(x) { var a = foo(), b = bar(), c = baz(); return a ? c : b; } + function c3(x) { var a = foo(), b = bar(), c = baz(); return b ? a : c; } + function c4(x) { var a = foo(), b = bar(), c = baz(); return b ? c : a; } + function c5(x) { var a = foo(), b = bar(), c = baz(); return c ? a : b; } + function c6(x) { var a = foo(), b = bar(), c = baz(); return c ? b : a; } + + function i1(x) { var a = foo(), b = bar(), c = baz(); if (a) return b; else return c; } + function i2(x) { var a = foo(), b = bar(), c = baz(); if (a) return c; else return b; } + function i3(x) { var a = foo(), b = bar(), c = baz(); if (b) return a; else return c; } + function i4(x) { var a = foo(), b = bar(), c = baz(); if (b) return c; else return a; } + function i5(x) { var a = foo(), b = bar(), c = baz(); if (c) return a; else return b; } + function i6(x) { var a = foo(), b = bar(), c = baz(); if (c) return b; else return a; } + } + expect: { + function c1(x) { var a = foo(), b = bar(), c = baz(); return a ? b : c; } + function c2(x) { var a = foo(), b = bar(), c = baz(); return a ? c : b; } + function c3(x) { var a = foo(), b = bar(), c = baz(); return b ? a : c; } + function c4(x) { var a = foo(), b = bar(), c = baz(); return b ? c : a; } + function c5(x) { var a = foo(), b = bar(); return baz() ? a : b; } + function c6(x) { var a = foo(), b = bar(); return baz() ? b : a; } + + function i1(x) { var a = foo(), b = bar(), c = baz(); if (a) return b; else return c; } + function i2(x) { var a = foo(), b = bar(), c = baz(); if (a) return c; else return b; } + function i3(x) { var a = foo(), b = bar(), c = baz(); if (b) return a; else return c; } + function i4(x) { var a = foo(), b = bar(), c = baz(); if (b) return c; else return a; } + function i5(x) { var a = foo(), b = bar(); if (baz()) return a; else return b; } + function i6(x) { var a = foo(), b = bar(); if (baz()) return b; else return a; } + } +} + From 7c3fee9e31e80fe60d1d776cdf81d9283490ff87 Mon Sep 17 00:00:00 2001 From: kzc Date: Fri, 29 Jan 2016 10:35:07 -0500 Subject: [PATCH 25/51] collapse_vars: avoid replacement across AST_Case nodes to be on safe side even though no issues seen. --- lib/compress.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compress.js b/lib/compress.js index 68471a5e..6fdf8f2d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -320,6 +320,7 @@ merge(Compressor.prototype, { if (node instanceof AST_Lambda || node instanceof AST_Try || node instanceof AST_With + || node instanceof AST_Case || node instanceof AST_IterationStatement || (parent instanceof AST_If && node !== parent.condition) || (parent instanceof AST_Conditional && node !== parent.condition) From a123e232b9e4d2fdc3905016fb56b71c5ab3ffcc Mon Sep 17 00:00:00 2001 From: Boris Letocha Date: Sun, 31 Jan 2016 21:41:38 +0100 Subject: [PATCH 26/51] Fixes #951 missing export for SymbolDef --- tools/exports.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/exports.js b/tools/exports.js index 5007e03b..110b5c4e 100644 --- a/tools/exports.js +++ b/tools/exports.js @@ -15,3 +15,4 @@ exports["parse"] = parse; exports["push_uniq"] = push_uniq; exports["string_template"] = string_template; exports["is_identifier"] = is_identifier; +exports["SymbolDef"] = SymbolDef; From cdba43cfa44c15fe12f87c356dad4caa5a946b5b Mon Sep 17 00:00:00 2001 From: Martii Date: Sat, 6 Feb 2016 12:46:18 -0700 Subject: [PATCH 27/51] Create and map `bare-returns` into new `parse` property name --- README.md | 11 ++++++++++- tools/node.js | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4bd1cab5..a145bb6c 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ The available options are: --noerr Don't throw an error for unknown options in -c, -b or -m. --bare-returns Allow return outside of functions. Useful when - minifying CommonJS modules. + minifying CommonJS modules and Userscripts that + may be anonymous function wrapped (IIFE) by the + .user.js engine `caller`. --keep-fnames Do not mangle/drop function names. Useful for code relying on Function.prototype.name. --reserved-file File containing reserved names @@ -636,6 +638,9 @@ Other options: - `compress` (default `{}`) — pass `false` to skip compressing entirely. Pass an object to specify custom [compressor options][compressor]. +- `parse` (default {}) — pass an object if you wish to specify some + additional [parser options][parser]. (not all options available... see below) + ##### mangleProperties options - `regex` — Pass a RegExp to only mangle certain names (maps to the `--mange-regex` CLI arguments option) @@ -658,6 +663,9 @@ properties are available: - `strict` — disable automatic semicolon insertion and support for trailing comma in arrays and objects +- `bare_returns` — Allow return outside of functions. (maps to the + `--bare-returns` CLI arguments option and available to `minify` `parse` + other options object) - `filename` — the name of the file where this code is coming from - `toplevel` — a `toplevel` node (as returned by a previous invocation of `parse`) @@ -797,3 +805,4 @@ The `source_map_options` (optional) can contain the following properties: [sm-spec]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit [codegen]: http://lisperator.net/uglifyjs/codegen [compressor]: http://lisperator.net/uglifyjs/compress + [parser]: http://lisperator.net/uglifyjs/parser diff --git a/tools/node.js b/tools/node.js index 5764286e..fa8c19dc 100644 --- a/tools/node.js +++ b/tools/node.js @@ -42,7 +42,8 @@ exports.minify = function(files, options) { mangleProperties : false, nameCache : null, output : null, - compress : {} + compress : {}, + parse : {} }); UglifyJS.base54.reset(); @@ -62,7 +63,8 @@ exports.minify = function(files, options) { sourcesContent[file] = code; toplevel = UglifyJS.parse(code, { filename: options.fromString ? i : file, - toplevel: toplevel + toplevel: toplevel, + bare_returns: options.parse ? options.parse.bare_returns : undefined }); }); } From d5c651a5e5667e64f4fe76f4d3669ba5bd375b6d Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Mon, 8 Feb 2016 10:36:28 +0100 Subject: [PATCH 28/51] Allow cli options to be specified in separate definitions Fix for #963. This allows stuff like `--define a=1 --define b=1` besides only `--define a=1,b=1` --- bin/uglifyjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index f7f22215..1f449aa9 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -499,17 +499,19 @@ function normalize(o) { } } -function getOptions(x, constants) { - x = ARGS[x]; +function getOptions(flag, constants) { + var x = ARGS[flag]; if (x == null) return null; var ret = {}; if (x !== "") { + if (Array.isArray(x)) x = x.map(function (v) { return "(" + v + ")"; }).join(", "); + var ast; try { ast = UglifyJS.parse(x, { expression: true }); } catch(ex) { if (ex instanceof UglifyJS.JS_Parse_Error) { - print_error("Error parsing arguments in: " + x); + print_error("Error parsing arguments for flag `" + flag + "': " + x); process.exit(1); } } @@ -529,7 +531,7 @@ function getOptions(x, constants) { return true; // no descend } print_error(node.TYPE) - print_error("Error parsing arguments in: " + x); + print_error("Error parsing arguments for flag `" + flag + "': " + x); process.exit(1); })); } From 7a4ed9d200c96d1fa2f9fcdfeab6ee76a3bbd696 Mon Sep 17 00:00:00 2001 From: sergeyv Date: Mon, 8 Feb 2016 13:42:07 -0800 Subject: [PATCH 29/51] Revert "using the original sourcemap as the base" This reverts commit ad18689d926d25c7a25b95c630c2ad05b7b5f5b5. Reason for revert: introduce issue #882 Currently, generated sourcemap contains copy of all existing mappings and adds new mappings from uglified code to original one. However, previous mapping are no longer valid and shouldn't be added. --- lib/sourcemap.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/sourcemap.js b/lib/sourcemap.js index a67011f0..e5d7df60 100644 --- a/lib/sourcemap.js +++ b/lib/sourcemap.js @@ -53,16 +53,11 @@ function SourceMap(options) { orig_line_diff : 0, dest_line_diff : 0, }); + var generator = new MOZ_SourceMap.SourceMapGenerator({ + file : options.file, + sourceRoot : options.root + }); var orig_map = options.orig && new MOZ_SourceMap.SourceMapConsumer(options.orig); - var generator; - if (orig_map) { - generator = MOZ_SourceMap.SourceMapGenerator.fromSourceMap(orig_map); - } else { - generator = new MOZ_SourceMap.SourceMapGenerator({ - file : options.file, - sourceRoot : options.root - }); - } function add(source, gen_line, gen_col, orig_line, orig_col, name) { if (orig_map) { var info = orig_map.originalPositionFor({ @@ -83,7 +78,7 @@ function SourceMap(options) { source : source, name : name }); - } + }; return { add : add, get : function() { return generator }, From 31a9b05c9642d8b402611d49ea23a6c2902cf374 Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Tue, 16 Feb 2016 18:15:59 +0800 Subject: [PATCH 30/51] Preserve ThisBinding in conditionals & collapse_vars Fixes #973 --- lib/compress.js | 39 ++++++++++++++++------------ test/compress/issue-782.js | 6 ++--- test/compress/issue-973.js | 52 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 test/compress/issue-973.js diff --git a/lib/compress.js b/lib/compress.js index 6fdf8f2d..16dc90f5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -176,6 +176,21 @@ merge(Compressor.prototype, { } }; + // we shouldn't compress (1,func)(something) to + // func(something) because that changes the meaning of + // the func (becomes lexical instead of global). + function maintain_this_binding(parent, orig, val) { + if (parent instanceof AST_Call && parent.expression === orig && val instanceof AST_PropAccess) { + return make_node(AST_Seq, orig, { + car: make_node(AST_Number, orig, { + value: 0 + }), + cdr: val + }); + } + return val; + } + function as_statement_array(thing) { if (thing === null) return []; if (thing instanceof AST_BlockStatement) return thing.body; @@ -366,7 +381,7 @@ merge(Compressor.prototype, { if (is_lvalue(node, parent)) return node; // Remove var definition and return its value to the TreeTransformer to replace. - var value = var_decl.value; + var value = maintain_this_binding(parent, node, var_decl.value); var_decl.value = null; var_defs.splice(var_defs_index, 1); @@ -2108,13 +2123,7 @@ merge(Compressor.prototype, { if (!compressor.option("side_effects")) return self; if (!self.car.has_side_effects(compressor)) { - // we shouldn't compress (1,func)(something) to - // func(something) because that changes the meaning of - // the func (becomes lexical instead of global). - var p = compressor.parent(); - if (!(p instanceof AST_Call && p.expression === self)) { - return self.cdr; - } + return maintain_this_binding(compressor.parent(), self, self.cdr); } if (compressor.option("cascade")) { if (self.car instanceof AST_Assign @@ -2304,11 +2313,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); - var rr = self.right.evaluate(compressor); - return rr[0]; + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } else { compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return ll[0]; + return maintain_this_binding(compressor.parent(), self, ll[0]); } } } @@ -2317,11 +2325,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); - return ll[0]; + return maintain_this_binding(compressor.parent(), self, ll[0]); } else { compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); - var rr = self.right.evaluate(compressor); - return rr[0]; + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } } } @@ -2546,10 +2553,10 @@ merge(Compressor.prototype, { if (cond.length > 1) { if (cond[1]) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); - return self.consequent; + return maintain_this_binding(compressor.parent(), self, self.consequent); } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.start); - return self.alternative; + return maintain_this_binding(compressor.parent(), self, self.alternative); } } var negated = cond[0].negate(compressor); diff --git a/test/compress/issue-782.js b/test/compress/issue-782.js index cce15fd1..80b1493c 100644 --- a/test/compress/issue-782.js +++ b/test/compress/issue-782.js @@ -5,19 +5,19 @@ remove_redundant_sequence_items: { (0, 1, _decorators.logThis)(); } expect: { - (0, logThis)(); + logThis(); (0, _decorators.logThis)(); } } -dont_remove_lexical_binding_sequence: { +dont_remove_this_binding_sequence: { options = { side_effects: true }; input: { (0, logThis)(); (0, _decorators.logThis)(); } expect: { - (0, logThis)(); + logThis(); (0, _decorators.logThis)(); } } diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js new file mode 100644 index 00000000..41fb0e25 --- /dev/null +++ b/test/compress/issue-973.js @@ -0,0 +1,52 @@ +this_binding_conditionals: { + options = { + conditionals: true, + evaluate : true + }; + input: { + (1 && a)(); + (0 || a)(); + (0 || 1 && a)(); + (1 ? a : 0)(); + + (1 && a.b)(); + (0 || a.b)(); + (0 || 1 && a.b)(); + (1 ? a.b : 0)(); + + (1 && a[b])(); + (0 || a[b])(); + (0 || 1 && a[b])(); + (1 ? a[b] : 0)(); + } + expect: { + a(); + a(); + a(); + a(); + + (0, a.b)(); + (0, a.b)(); + (0, a.b)(); + (0, a.b)(); + + (0, a[b])(); + (0, a[b])(); + (0, a[b])(); + (0, a[b])(); + } +} + +this_binding_collapse_vars: { + options = { + collapse_vars: true, + }; + input: { + var c = a; c(); + var d = a.b; d(); + } + expect: { + a(); + (0, a.b)(); + } +} \ No newline at end of file From 9662228f6a026908aaf6f3e3531b6abd98fa28fc Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 16 Feb 2016 19:00:48 +0100 Subject: [PATCH 31/51] Don't compress (0, eval)() to eval() --- lib/compress.js | 34 ++++++++++++++++++---------------- test/compress/issue-973.js | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 16dc90f5..df8975d5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -179,14 +179,16 @@ merge(Compressor.prototype, { // we shouldn't compress (1,func)(something) to // func(something) because that changes the meaning of // the func (becomes lexical instead of global). - function maintain_this_binding(parent, orig, val) { - if (parent instanceof AST_Call && parent.expression === orig && val instanceof AST_PropAccess) { - return make_node(AST_Seq, orig, { - car: make_node(AST_Number, orig, { - value: 0 - }), - cdr: val - }); + function maintain_call_binding(parent, orig, val) { + if (parent instanceof AST_Call && parent.expression === orig) { + if (val instanceof AST_PropAccess || (val instanceof AST_Symbol && val.name === "eval")) { + return make_node(AST_Seq, orig, { + car: make_node(AST_Number, orig, { + value: 0 + }), + cdr: val + }); + } } return val; } @@ -381,7 +383,7 @@ merge(Compressor.prototype, { if (is_lvalue(node, parent)) return node; // Remove var definition and return its value to the TreeTransformer to replace. - var value = maintain_this_binding(parent, node, var_decl.value); + var value = maintain_call_binding(parent, node, var_decl.value); var_decl.value = null; var_defs.splice(var_defs_index, 1); @@ -2123,7 +2125,7 @@ merge(Compressor.prototype, { if (!compressor.option("side_effects")) return self; if (!self.car.has_side_effects(compressor)) { - return maintain_this_binding(compressor.parent(), self, self.cdr); + return maintain_call_binding(compressor.parent(), self, self.cdr); } if (compressor.option("cascade")) { if (self.car instanceof AST_Assign @@ -2313,10 +2315,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); + return maintain_call_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } else { compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, ll[0]); + return maintain_call_binding(compressor.parent(), self, ll[0]); } } } @@ -2325,10 +2327,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, ll[0]); + return maintain_call_binding(compressor.parent(), self, ll[0]); } else { compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); + return maintain_call_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } } } @@ -2553,10 +2555,10 @@ merge(Compressor.prototype, { if (cond.length > 1) { if (cond[1]) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.consequent); + return maintain_call_binding(compressor.parent(), self, self.consequent); } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.start); - return maintain_this_binding(compressor.parent(), self, self.alternative); + return maintain_call_binding(compressor.parent(), self, self.alternative); } } var negated = cond[0].negate(compressor); diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js index 41fb0e25..f2d44010 100644 --- a/test/compress/issue-973.js +++ b/test/compress/issue-973.js @@ -49,4 +49,21 @@ this_binding_collapse_vars: { a(); (0, a.b)(); } +} + +eval_direct_calls: { + options = { + side_effects: true, + collapse_vars: true + } + input: { + (0, eval)(''); + + var fn = eval; + fn(''); + } + expect: { + (0, eval)(''); + (0, eval)(''); + } } \ No newline at end of file From 654743772514f5204d38bc7e7dba566b9dafedbd Mon Sep 17 00:00:00 2001 From: alexlamsl Date: Wed, 17 Feb 2016 02:52:28 +0800 Subject: [PATCH 32/51] preserve ThisBinding for side_effects --- lib/compress.js | 20 ++++----- lib/scope.js | 9 ++-- test/compress/issue-782.js | 4 ++ test/compress/issue-973.js | 47 +++++++++++++++----- test/compress/issue-976.js | 88 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 test/compress/issue-976.js diff --git a/lib/compress.js b/lib/compress.js index df8975d5..ed4f62ff 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -179,9 +179,9 @@ merge(Compressor.prototype, { // we shouldn't compress (1,func)(something) to // func(something) because that changes the meaning of // the func (becomes lexical instead of global). - function maintain_call_binding(parent, orig, val) { + function maintain_this_binding(parent, orig, val) { if (parent instanceof AST_Call && parent.expression === orig) { - if (val instanceof AST_PropAccess || (val instanceof AST_Symbol && val.name === "eval")) { + if (val instanceof AST_PropAccess || val instanceof AST_SymbolRef && val.name === "eval") { return make_node(AST_Seq, orig, { car: make_node(AST_Number, orig, { value: 0 @@ -383,7 +383,7 @@ merge(Compressor.prototype, { if (is_lvalue(node, parent)) return node; // Remove var definition and return its value to the TreeTransformer to replace. - var value = maintain_call_binding(parent, node, var_decl.value); + var value = maintain_this_binding(parent, node, var_decl.value); var_decl.value = null; var_defs.splice(var_defs_index, 1); @@ -2125,7 +2125,7 @@ merge(Compressor.prototype, { if (!compressor.option("side_effects")) return self; if (!self.car.has_side_effects(compressor)) { - return maintain_call_binding(compressor.parent(), self, self.cdr); + return maintain_this_binding(compressor.parent(), self, self.cdr); } if (compressor.option("cascade")) { if (self.car instanceof AST_Assign @@ -2315,10 +2315,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of && always true [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } else { compressor.warn("Condition left of && always false [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, ll[0]); + return maintain_this_binding(compressor.parent(), self, ll[0]); } } } @@ -2327,10 +2327,10 @@ merge(Compressor.prototype, { if (ll.length > 1) { if (ll[1]) { compressor.warn("Condition left of || always true [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, ll[0]); + return maintain_this_binding(compressor.parent(), self, ll[0]); } else { compressor.warn("Condition left of || always false [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); + return maintain_this_binding(compressor.parent(), self, self.right.evaluate(compressor)[0]); } } } @@ -2555,10 +2555,10 @@ merge(Compressor.prototype, { if (cond.length > 1) { if (cond[1]) { compressor.warn("Condition always true [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, self.consequent); + return maintain_this_binding(compressor.parent(), self, self.consequent); } else { compressor.warn("Condition always false [{file}:{line},{col}]", self.start); - return maintain_call_binding(compressor.parent(), self, self.alternative); + return maintain_this_binding(compressor.parent(), self, self.alternative); } } var negated = cond[0].negate(compressor); diff --git a/lib/scope.js b/lib/scope.js index 20d9d730..ace25894 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -194,6 +194,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolRef) { var name = node.name; + if (name == "eval" && tw.parent() instanceof AST_Call) { + for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) { + s.uses_eval = true; + } + } var sym = node.scope.find_variable(name); if (!sym) { var g; @@ -206,10 +211,6 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ globals.set(name, g); } node.thedef = g; - if (name == "eval" && tw.parent() instanceof AST_Call) { - for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) - s.uses_eval = true; - } if (func && name == "arguments") { func.uses_arguments = true; } diff --git a/test/compress/issue-782.js b/test/compress/issue-782.js index 80b1493c..2f72d1ab 100644 --- a/test/compress/issue-782.js +++ b/test/compress/issue-782.js @@ -1,10 +1,12 @@ remove_redundant_sequence_items: { options = { side_effects: true }; input: { + (0, 1, eval)(); (0, 1, logThis)(); (0, 1, _decorators.logThis)(); } expect: { + (0, eval)(); logThis(); (0, _decorators.logThis)(); } @@ -13,10 +15,12 @@ remove_redundant_sequence_items: { dont_remove_this_binding_sequence: { options = { side_effects: true }; input: { + (0, eval)(); (0, logThis)(); (0, _decorators.logThis)(); } expect: { + (0, eval)(); logThis(); (0, _decorators.logThis)(); } diff --git a/test/compress/issue-973.js b/test/compress/issue-973.js index f2d44010..0e040922 100644 --- a/test/compress/issue-973.js +++ b/test/compress/issue-973.js @@ -18,6 +18,11 @@ this_binding_conditionals: { (0 || a[b])(); (0 || 1 && a[b])(); (1 ? a[b] : 0)(); + + (1 && eval)(); + (0 || eval)(); + (0 || 1 && eval)(); + (1 ? eval : 0)(); } expect: { a(); @@ -34,6 +39,11 @@ this_binding_conditionals: { (0, a[b])(); (0, a[b])(); (0, a[b])(); + + (0, eval)(); + (0, eval)(); + (0, eval)(); + (0, eval)(); } } @@ -44,26 +54,43 @@ this_binding_collapse_vars: { input: { var c = a; c(); var d = a.b; d(); + var e = eval; e(); } expect: { a(); (0, a.b)(); + (0, eval)(); } } -eval_direct_calls: { +this_binding_side_effects: { options = { - side_effects: true, - collapse_vars: true - } + side_effects : true + }; input: { - (0, eval)(''); - - var fn = eval; - fn(''); + (function (foo) { + (0, foo)(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + (function (foo) { + var eval = console; + (0, foo)(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); } expect: { - (0, eval)(''); - (0, eval)(''); + (function (foo) { + foo(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); + (function (foo) { + var eval = console; + foo(); + (0, foo.bar)(); + (0, eval)('console.log(foo);'); + }()); } } \ No newline at end of file diff --git a/test/compress/issue-976.js b/test/compress/issue-976.js new file mode 100644 index 00000000..dea30705 --- /dev/null +++ b/test/compress/issue-976.js @@ -0,0 +1,88 @@ +eval_collapse_vars: { + options = { + collapse_vars:true, sequences:false, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + }; + input: { + function f1() { + var e = 7; + var s = "abcdef"; + var i = 2; + var eval = console.log.bind(console); + var x = s.charAt(i++); + var y = s.charAt(i++); + var z = s.charAt(i++); + eval(x, y, z, e); + } + function p1() { var a = foo(), b = bar(), eval = baz(); return a + b + eval; } + function p2() { var a = foo(), b = bar(), eval = baz; return a + b + eval(); } + (function f2(eval) { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(eval); + } + expect: { + function f1() { + var e = 7, + s = "abcdef", + i = 2, + eval = console.log.bind(console), + x = s.charAt(i++), + y = s.charAt(i++), + z = s.charAt(i++); + eval(x, y, z, e); + } + function p1() { return foo() + bar() + baz(); } + function p2() { var a = foo(), b = bar(), eval = baz; return a + b + eval(); } + (function f2(eval) { + var a = 2; + console.log(a - 5); + eval("console.log(a);"); + })(eval); + } +} + +eval_unused: { + options = { unused: true, keep_fargs: false }; + input: { + function f1(a, eval, c, d, e) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } + expect: { + function f1(a, eval) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } +} + +eval_mangle: { + mangle = { + }; + input: { + function f1(a, eval, c, d, e) { + return a('c') + eval; + } + function f2(a, b, c, d, e) { + return a + eval('c'); + } + function f3(a, eval, c, d, e) { + return a + eval('c'); + } + } + expect_exact: 'function f1(n,c,e,a,o){return n("c")+c}function f2(a,b,c,d,e){return a+eval("c")}function f3(a,eval,c,d,e){return a+eval("c")}' +} From bdd8e34f635db60765f1fd5c8b5355e71ee16095 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Wed, 17 Feb 2016 20:04:45 +0100 Subject: [PATCH 33/51] Allow --no-* options to disable their respective parameter Fixes #974 and #972 --- bin/uglifyjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/uglifyjs b/bin/uglifyjs index 1f449aa9..90197cc4 100755 --- a/bin/uglifyjs +++ b/bin/uglifyjs @@ -501,7 +501,7 @@ function normalize(o) { function getOptions(flag, constants) { var x = ARGS[flag]; - if (x == null) return null; + if (x == null || x === false) return null; var ret = {}; if (x !== "") { if (Array.isArray(x)) x = x.map(function (v) { return "(" + v + ")"; }).join(", "); From 5486b68850f8da3fbcdef228a88ff494c33bac4a Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 21 Feb 2016 12:05:02 -0500 Subject: [PATCH 34/51] Take operator || precendence into account for AST_If optimization. Fixes #979. --- lib/compress.js | 13 +++++- test/compress/issue-979.js | 89 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-979.js diff --git a/lib/compress.js b/lib/compress.js index ed4f62ff..a2666fa9 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1688,9 +1688,13 @@ merge(Compressor.prototype, { } if (is_empty(self.alternative)) self.alternative = null; var negated = self.condition.negate(compressor); - var negated_is_best = best_of(self.condition, negated) === negated; + var self_condition_length = self.condition.print_to_string().length; + var negated_length = negated.print_to_string().length; + var negated_is_best = negated_length < self_condition_length; if (self.alternative && negated_is_best) { negated_is_best = false; // because we already do the switch here. + // no need to swap values of self_condition_length and negated_length + // here because they are only used in an equality comparison later on. self.condition = negated; var tmp = self.body; self.body = self.alternative || make_node(AST_EmptyStatement); @@ -1712,6 +1716,13 @@ merge(Compressor.prototype, { }).transform(compressor); } if (is_empty(self.alternative) && self.body instanceof AST_SimpleStatement) { + if (self_condition_length === negated_length && !negated_is_best + && self.condition instanceof AST_Binary && self.condition.operator == "||") { + // although the code length of self.condition and negated are the same, + // negated does not require additional surrounding parentheses. + // see https://github.com/mishoo/UglifyJS2/issues/979 + negated_is_best = true; + } if (negated_is_best) return make_node(AST_SimpleStatement, self, { body: make_node(AST_Binary, self, { operator : "||", diff --git a/test/compress/issue-979.js b/test/compress/issue-979.js new file mode 100644 index 00000000..bae15db8 --- /dev/null +++ b/test/compress/issue-979.js @@ -0,0 +1,89 @@ +issue979_reported: { + options = { + sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { + if (a == 1 || b == 2) { + foo(); + } + } + function f2() { + if (!(a == 1 || b == 2)) { + } + else { + foo(); + } + } + } + expect: { + function f1() { + 1!=a&&2!=b||foo(); + } + function f2() { + 1!=a&&2!=b||foo(); + } + } +} + +issue979_test_negated_is_best: { + options = { + sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f3() { + if (a == 1 | b == 2) { + foo(); + } + } + function f4() { + if (!(a == 1 | b == 2)) { + } + else { + foo(); + } + } + function f5() { + if (a == 1 && b == 2) { + foo(); + } + } + function f6() { + if (!(a == 1 && b == 2)) { + } + else { + foo(); + } + } + function f7() { + if (a == 1 || b == 2) { + foo(); + } + else { + return bar(); + } + } + } + expect: { + function f3() { + 1==a|2==b&&foo(); + } + function f4() { + 1==a|2==b&&foo(); + } + function f5() { + 1==a&&2==b&&foo(); + } + function f6() { + 1!=a||2!=b||foo(); + } + function f7() { + return 1!=a&&2!=b?bar():void foo(); + } + } +} + From 11b0efdf84dcdae71ac66453f8bf644052a32cc8 Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 21 Feb 2016 18:47:21 -0500 Subject: [PATCH 35/51] boolean_expression ? true : false --> boolean_expression --- lib/compress.js | 4 ++ test/compress/conditionals.js | 74 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index a2666fa9..f49486a0 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2651,6 +2651,10 @@ merge(Compressor.prototype, { // y?true:false --> !!y if (is_true(consequent) && is_false(alternative)) { + if (self.condition.is_boolean()) { + // boolean_expression ? true : false --> boolean_expression + return self.condition; + } self.condition = self.condition.negate(compressor); return make_node(AST_UnaryPrefix, self.condition, { operator: "!", diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index 65cfea64..db0d8000 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -738,3 +738,77 @@ conditional_or: { a = condition + 3 || null; } } + +trivial_boolean_ternary_expressions : { + options = { + conditionals: true, + evaluate : true, + booleans : true + }; + input: { + f('foo' in m ? true : false); + f('foo' in m ? false : true); + + f(g ? true : false); + f(foo() ? true : false); + f("bar" ? true : false); + f(5 ? true : false); + f(5.7 ? true : false); + f(x - y ? true : false); + + f(x == y ? true : false); + f(x === y ? !0 : !1); + f(x < y ? !0 : false); + f(x <= y ? true : false); + f(x > y ? true : !1); + f(x >= y ? !0 : !1); + + f(g ? false : true); + f(foo() ? false : true); + f("bar" ? false : true); + f(5 ? false : true); + f(5.7 ? false : true); + f(x - y ? false : true); + + f(x == y ? !1 : !0); + f(x === y ? false : true); + + f(x < y ? false : true); + f(x <= y ? false : !0); + f(x > y ? !1 : true); + f(x >= y ? !1 : !0); + } + expect: { + f('foo' in m); + f(!('foo' in m)); + + f(!!g); + f(!!foo()); + f(!0); + f(!0); + f(!0); + f(!!(x - y)); + + f(x == y); + f(x === y); + f(x < y); + f(x <= y); + f(x > y); + f(x >= y); + + f(!g); + f(!foo()); + f(!1); + f(!1); + f(!1); + f(!(x - y)); + + f(x != y); + f(x !== y); + + f(!(x < y)); + f(!(x <= y)); + f(!(x > y)); + f(!(x >= y)); + } +} From 294861ba96aaf61591e2158c9e9ffad50f58625d Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Mon, 22 Feb 2016 21:39:14 +0200 Subject: [PATCH 36/51] v2.6.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index da0f835e..748e04fb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "homepage": "http://lisperator.net/uglifyjs", "author": "Mihai Bazon (http://lisperator.net/)", "license": "BSD-2-Clause", - "version": "2.6.1", + "version": "2.6.2", "engines": { "node": ">=0.8.0" }, From 102d1b9137353086f11fe2fc7abf9095042d4306 Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 27 Feb 2016 15:33:10 -0500 Subject: [PATCH 37/51] #877 Ignore mangle sort option --- README.md | 5 ----- lib/scope.js | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 4012b755..92559019 100644 --- a/README.md +++ b/README.md @@ -192,11 +192,6 @@ input files from the command line. To enable the mangler you need to pass `--mangle` (`-m`). The following (comma-separated) options are supported: -- `sort` — to assign shorter names to most frequently used variables. This - saves a few hundred bytes on jQuery before gzip, but the output is - _bigger_ after gzip (and seems to happen for other libraries I tried it - on) therefore it's not enabled by default. - - `toplevel` — mangle names declared in the toplevel scope (disabled by default). diff --git a/lib/scope.js b/lib/scope.js index ace25894..5ab775a3 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -372,7 +372,7 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ return defaults(options, { except : [], eval : false, - sort : false, + sort : false, // Ignored. Flag retained for backwards compatibility. toplevel : false, screw_ie8 : false, keep_fnames : false @@ -415,9 +415,6 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ a.push(symbol); } }); - if (options.sort) a.sort(function(a, b){ - return b.references.length - a.references.length; - }); to_mangle.push.apply(to_mangle, a); return; } From ee6c9fabb7dc00b8bbc9e60859767acc585cd0fa Mon Sep 17 00:00:00 2001 From: philippsimon Date: Mon, 14 Mar 2016 12:21:25 +0100 Subject: [PATCH 38/51] Fix: Uglified Number.prototype functions on big numbers --- lib/output.js | 10 +++++++--- test/compress/numbers.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 test/compress/numbers.js diff --git a/lib/output.js b/lib/output.js index dceece34..a8c45a0f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -596,8 +596,12 @@ function OutputStream(options) { PARENS(AST_Number, function(output){ var p = output.parent(); - if (this.getValue() < 0 && p instanceof AST_PropAccess && p.expression === this) - return true; + if (p instanceof AST_PropAccess && p.expression === this) { + var value = this.getValue(); + if (value < 0 || /^0/.test(make_num(value))) { + return true; + } + } }); PARENS([ AST_Assign, AST_Conditional ], function (output){ @@ -1026,7 +1030,7 @@ function OutputStream(options) { var expr = self.expression; expr.print(output); if (expr instanceof AST_Number && expr.getValue() >= 0) { - if (!/[xa-f.]/i.test(output.last())) { + if (!/[xa-f.)]/i.test(output.last())) { output.print("."); } } diff --git a/test/compress/numbers.js b/test/compress/numbers.js new file mode 100644 index 00000000..8e32ad02 --- /dev/null +++ b/test/compress/numbers.js @@ -0,0 +1,19 @@ +hex_numbers_in_parentheses_for_prototype_functions: { + input: { + (-2); + (-2).toFixed(0); + + (2); + (2).toFixed(0); + + (0.2); + (0.2).toFixed(0); + + (0.00000002); + (0.00000002).toFixed(0); + + (1000000000000000128); + (1000000000000000128).toFixed(0); + } + expect_exact: "-2;(-2).toFixed(0);2;2..toFixed(0);.2;.2.toFixed(0);2e-8;2e-8.toFixed(0);0xde0b6b3a7640080;(0xde0b6b3a7640080).toFixed(0);" +} From a9d4a6291b2f3c2531ff4bdd59ac75edd0a06d60 Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 15 Mar 2016 11:20:32 -0400 Subject: [PATCH 39/51] Do not produce `let` as a variable name in mangle. Would previously occur in large generated functions with 21,000+ variables. Fixes #986. --- lib/parse.js | 2 +- test/mocha/let.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/mocha/let.js diff --git a/lib/parse.js b/lib/parse.js index f1495153..f16d092b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -46,7 +46,7 @@ var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var void while with'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char class double enum export extends final float goto implements import int interface let long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; diff --git a/test/mocha/let.js b/test/mocha/let.js new file mode 100644 index 00000000..c4ffe389 --- /dev/null +++ b/test/mocha/let.js @@ -0,0 +1,26 @@ +var Uglify = require('../../'); +var assert = require("assert"); + +describe("let", function() { + it("Should not produce `let` as a variable name in mangle", function() { + // Produce a lot of variables in a function and run it through mangle. + var s = '"use strict"; function foo() {'; + for (var i = 0; i < 21000; ++i) { + s += "var v" + i + "=0;"; + } + s += '}'; + var result = Uglify.minify(s, {fromString: true, compress: false}); + + // Verify that select keywords and reserved keywords not produced + assert.strictEqual(result.code.indexOf("var let="), -1); + assert.strictEqual(result.code.indexOf("var do="), -1); + assert.strictEqual(result.code.indexOf("var var="), -1); + + // Verify that the variable names that appeared immediately before + // and after the erroneously generated `let` variable name still exist + // to show the test generated enough symbols. + assert(result.code.indexOf("var ket=") >= 0); + assert(result.code.indexOf("var met=") >= 0); + }); +}); + From 21befe583ffac6a9c27c8e9d67eae39feb5b528f Mon Sep 17 00:00:00 2001 From: kzc Date: Tue, 15 Mar 2016 11:44:09 -0400 Subject: [PATCH 40/51] Attempt to increase timeout for mocha let test. --- test/mocha/let.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/mocha/let.js b/test/mocha/let.js index c4ffe389..89fd9f1a 100644 --- a/test/mocha/let.js +++ b/test/mocha/let.js @@ -2,7 +2,9 @@ var Uglify = require('../../'); var assert = require("assert"); describe("let", function() { - it("Should not produce `let` as a variable name in mangle", function() { + it("Should not produce `let` as a variable name in mangle", function(done) { + this.timeout(10000); + // Produce a lot of variables in a function and run it through mangle. var s = '"use strict"; function foo() {'; for (var i = 0; i < 21000; ++i) { @@ -21,6 +23,8 @@ describe("let", function() { // to show the test generated enough symbols. assert(result.code.indexOf("var ket=") >= 0); assert(result.code.indexOf("var met=") >= 0); + + done(); }); }); From 07bb7262d02547cef5ffaca71d49c86640a1df15 Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 24 Mar 2016 11:51:54 -0400 Subject: [PATCH 41/51] Escape all ASCII control characters within strings when using ascii_only. Fixes #1017. Tab characters within strings are now output as `\t` in all output modes. --- lib/output.js | 11 ++++++----- test/compress/ascii.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 test/compress/ascii.js diff --git a/lib/output.js b/lib/output.js index a8c45a0f..a59066fc 100644 --- a/lib/output.js +++ b/lib/output.js @@ -74,7 +74,7 @@ function OutputStream(options) { var OUTPUT = ""; function to_ascii(str, identifier) { - return str.replace(/[\u0080-\uffff]/g, function(ch) { + return str.replace(/[\u0000-\u001f\u007f-\uffff]/g, function(ch) { var code = ch.charCodeAt(0).toString(16); if (code.length <= 2 && !identifier) { while (code.length < 2) code = "0" + code; @@ -90,16 +90,17 @@ function OutputStream(options) { var dq = 0, sq = 0; str = str.replace(/[\\\b\f\n\r\v\t\x22\x27\u2028\u2029\0\ufeff]/g, function(s){ switch (s) { + case '"': ++dq; return '"'; + case "'": ++sq; return "'"; case "\\": return "\\\\"; - case "\b": return "\\b"; - case "\f": return "\\f"; case "\n": return "\\n"; case "\r": return "\\r"; + case "\t": return "\\t"; + case "\b": return "\\b"; + case "\f": return "\\f"; case "\x0B": return options.screw_ie8 ? "\\v" : "\\x0B"; case "\u2028": return "\\u2028"; case "\u2029": return "\\u2029"; - case '"': ++dq; return '"'; - case "'": ++sq; return "'"; case "\0": return "\\x00"; case "\ufeff": return "\\ufeff"; } diff --git a/test/compress/ascii.js b/test/compress/ascii.js new file mode 100644 index 00000000..5c6b3b8e --- /dev/null +++ b/test/compress/ascii.js @@ -0,0 +1,32 @@ +ascii_only_true: { + options = {} + beautify = { + ascii_only : true, + beautify : false, + } + input: { + function f() { + return "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + + "\x20\x21\x22\x23 ... \x7d\x7e\x7f\x80\x81 ... \xfe\xff\u0fff\uffff"; + } + } + expect_exact: 'function f(){return"\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\b\\t\\n\\x0B\\f\\r\\x0e\\x0f"+"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f"+\' !"# ... }~\\x7f\\x80\\x81 ... \\xfe\\xff\\u0fff\\uffff\'}' +} + +ascii_only_false: { + options = {} + beautify = { + ascii_only : false, + beautify : false, + } + input: { + function f() { + return "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + + "\x20\x21\x22\x23 ... \x7d\x7e\x7f\x80\x81 ... \xfe\xff\u0fff\uffff"; + } + } + expect_exact: 'function f(){return"\\x00\x01\x02\x03\x04\x05\x06\x07\\b\\t\\n\\x0B\\f\\r\x0e\x0f"+"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"+\' !"# ... }~\x7f\x80\x81 ... \xfe\xff\u0fff\uffff\'}' +} + From 9bcf702a6e12ab35fda9ca35042ae3dc7c449891 Mon Sep 17 00:00:00 2001 From: Sebastien Daniel Date: Fri, 4 Mar 2016 07:32:24 -0500 Subject: [PATCH 42/51] added documentation on conditional compilation using API --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 92559019..9118d663 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,22 @@ code as usual. The build will contain the `const` declarations if you use them. If you are targeting < ES6 environments, use `/** @const */ var`. + +#### Conditional compilation, API +You can also use conditional compilation via the programmatic API. With the difference that the +property name is `global_defs` and is a compressor property: + +```js +uglifyJS.minify([ "input.js"], { + compress: { + dead_code: true, + global_defs: { + DEBUG: false + } + } +}); +``` + ## Beautifier options The code generator tries to output shortest code possible by default. In From 45ddb9caeb200615b9f0ced0ed83fa95f0c51b23 Mon Sep 17 00:00:00 2001 From: kzc Date: Mon, 28 Mar 2016 17:58:50 -0400 Subject: [PATCH 43/51] Speedup `unused` compress option for already minified code Fixes: #321 #917 #1022 --- lib/compress.js | 17 +++++++++++++---- lib/scope.js | 3 +++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f49486a0..4f37e834 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1242,6 +1242,7 @@ merge(Compressor.prototype, { && !self.uses_eval ) { var in_use = []; + var in_use_ids = {}; // avoid expensive linear scans of in_use var initializations = new Dictionary(); // pass 1: find out which symbols are directly used in // this scope (not in nested scopes). @@ -1264,7 +1265,11 @@ merge(Compressor.prototype, { return true; } if (node instanceof AST_SymbolRef) { - push_uniq(in_use, node.definition()); + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); + } return true; } if (node instanceof AST_Scope) { @@ -1287,7 +1292,11 @@ merge(Compressor.prototype, { if (init) init.forEach(function(init){ var tw = new TreeWalker(function(node){ if (node instanceof AST_SymbolRef) { - push_uniq(in_use, node.definition()); + var node_def = node.definition(); + if (!(node_def.id in in_use_ids)) { + in_use_ids[node_def.id] = true; + in_use.push(node_def); + } } }); init.walk(tw); @@ -1315,7 +1324,7 @@ merge(Compressor.prototype, { } } if (node instanceof AST_Defun && node !== self) { - if (!member(node.name.definition(), in_use)) { + if (!(node.name.definition().id in in_use_ids)) { compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { name : node.name.name, file : node.name.start.file, @@ -1328,7 +1337,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ - if (member(def.name.definition(), in_use)) return true; + if (def.name.definition().id in in_use_ids) return true; var w = { name : def.name.name, file : def.name.start.file, diff --git a/lib/scope.js b/lib/scope.js index 5ab775a3..d07eca12 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -53,8 +53,11 @@ function SymbolDef(scope, index, orig) { this.undeclared = false; this.constant = false; this.index = index; + this.id = SymbolDef.next_id++; }; +SymbolDef.next_id = 1; + SymbolDef.prototype = { unmangleable: function(options) { if (!options) options = {}; From 98434258d0839cb02de8b723df0cafe40c6cf218 Mon Sep 17 00:00:00 2001 From: kzc Date: Sat, 2 Apr 2016 00:21:13 -0400 Subject: [PATCH 44/51] Optimize ternaries with boolean consequent or alternative. Fixes #511 --- lib/compress.js | 58 +++++++++++++++++++++++------ test/compress/conditionals.js | 70 +++++++++++++++++++++++++++++++---- 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 4f37e834..26c11bd9 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2658,24 +2658,58 @@ merge(Compressor.prototype, { } } - // y?true:false --> !!y - if (is_true(consequent) && is_false(alternative)) { - if (self.condition.is_boolean()) { - // boolean_expression ? true : false --> boolean_expression - return self.condition; + if (is_true(self.consequent)) { + if (is_false(self.alternative)) { + // c ? true : false ---> !!c + return booleanize(self.condition); } - self.condition = self.condition.negate(compressor); - return make_node(AST_UnaryPrefix, self.condition, { - operator: "!", - expression: self.condition + // c ? true : x ---> !!c || x + return make_node(AST_Binary, self, { + operator: "||", + left: booleanize(self.condition), + right: self.alternative }); } - // y?false:true --> !y - if (is_false(consequent) && is_true(alternative)) { - return self.condition.negate(compressor) + if (is_false(self.consequent)) { + if (is_true(self.alternative)) { + // c ? false : true ---> !c + return booleanize(self.condition.negate(compressor)); + } + // c ? false : x ---> !c && x + return make_node(AST_Binary, self, { + operator: "&&", + left: booleanize(self.condition.negate(compressor)), + right: self.alternative + }); } + if (is_true(self.alternative)) { + // c ? x : true ---> !c || x + return make_node(AST_Binary, self, { + operator: "||", + left: booleanize(self.condition.negate(compressor)), + right: self.consequent + }); + } + if (is_false(self.alternative)) { + // c ? x : false ---> !!c && x + return make_node(AST_Binary, self, { + operator: "&&", + left: booleanize(self.condition), + right: self.consequent + }); + } + return self; + function booleanize(node) { + if (node.is_boolean()) return node; + // !!expression + return make_node(AST_UnaryPrefix, node, { + operator: "!", + expression: node.negate(compressor) + }); + } + // AST_True or !0 function is_true(node) { return node instanceof AST_True diff --git a/test/compress/conditionals.js b/test/compress/conditionals.js index db0d8000..f5eeb6f2 100644 --- a/test/compress/conditionals.js +++ b/test/compress/conditionals.js @@ -407,8 +407,8 @@ cond_8: { a = !condition; a = !condition; - a = condition ? 1 : false; - a = condition ? 0 : true; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } @@ -490,8 +490,8 @@ cond_8b: { a = !condition; a = !condition; - a = condition ? 1 : !1; - a = condition ? 0 : !0; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } @@ -557,7 +557,7 @@ cond_8c: { a = !!condition; a = !condition; - a = condition() ? !0 : !-3.5; + a = !!condition() || !-3.5; a = !!condition; a = !!condition; @@ -573,12 +573,68 @@ cond_8c: { a = !condition; a = !condition; - a = condition ? 1 : false; - a = condition ? 0 : true; + a = !!condition && 1; + a = !condition || 0; a = condition ? 1 : 0; } } +ternary_boolean_consequent: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { return a == b ? true : x; } + function f2() { return a == b ? false : x; } + function f3() { return a < b ? !0 : x; } + function f4() { return a < b ? !1 : x; } + function f5() { return c ? !0 : x; } + function f6() { return c ? false : x; } + function f7() { return !c ? true : x; } + function f8() { return !c ? !1 : x; } + } + expect: { + function f1() { return a == b || x; } + function f2() { return a != b && x; } + function f3() { return a < b || x; } + function f4() { return !(a < b) && x; } + function f5() { return !!c || x; } + function f6() { return !c && x; } + function f7() { return !c || x; } + function f8() { return !!c && x; } + } +} + +ternary_boolean_alternative: { + options = { + collapse_vars:true, sequences:true, properties:true, dead_code:true, conditionals:true, + comparisons:true, evaluate:true, booleans:true, loops:true, unused:true, hoist_funs:true, + keep_fargs:true, if_return:true, join_vars:true, cascade:true, side_effects:true + } + input: { + function f1() { return a == b ? x : true; } + function f2() { return a == b ? x : false; } + function f3() { return a < b ? x : !0; } + function f4() { return a < b ? x : !1; } + function f5() { return c ? x : true; } + function f6() { return c ? x : !1; } + function f7() { return !c ? x : !0; } + function f8() { return !c ? x : false; } + } + expect: { + function f1() { return a != b || x; } + function f2() { return a == b && x; } + function f3() { return !(a < b) || x; } + function f4() { return a < b && x; } + function f5() { return !c || x; } + function f6() { return !!c && x; } + function f7() { return !!c || x; } + function f8() { return !c && x; } + } +} + conditional_and: { options = { conditionals: true, From 931723737204d0a042604f8051ca3af8aa08a1c7 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Thu, 7 Apr 2016 13:15:28 +0300 Subject: [PATCH 45/51] Avoid using inherited hasOwnProperty Fix #1031 --- lib/ast.js | 2 +- lib/compress.js | 2 +- lib/utils.js | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0ac14dc9..2a461834 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -71,7 +71,7 @@ function DEFNODE(type, props, methods, base) { if (type) { ctor.prototype.TYPE = ctor.TYPE = type; } - if (methods) for (i in methods) if (methods.hasOwnProperty(i)) { + if (methods) for (i in methods) if (HOP(methods, i)) { if (/^\$/.test(i)) { ctor[i.substr(1)] = methods[i]; } else { diff --git a/lib/compress.js b/lib/compress.js index 26c11bd9..d42f84d1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2509,7 +2509,7 @@ merge(Compressor.prototype, { if (self.undeclared() && !isLHS(self, compressor.parent())) { var defines = compressor.option("global_defs"); - if (defines && defines.hasOwnProperty(self.name)) { + if (defines && HOP(defines, self.name)) { return make_node_from_constant(compressor, defines[self.name], self); } switch (self.name) { diff --git a/lib/utils.js b/lib/utils.js index 4612a322..c81ca71f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -97,17 +97,17 @@ function defaults(args, defs, croak) { if (args === true) args = {}; var ret = args || {}; - if (croak) for (var i in ret) if (ret.hasOwnProperty(i) && !defs.hasOwnProperty(i)) + if (croak) for (var i in ret) if (HOP(ret, i) && !HOP(defs, i)) DefaultsError.croak("`" + i + "` is not a supported option", defs); - for (var i in defs) if (defs.hasOwnProperty(i)) { - ret[i] = (args && args.hasOwnProperty(i)) ? args[i] : defs[i]; + for (var i in defs) if (HOP(defs, i)) { + ret[i] = (args && HOP(args, i)) ? args[i] : defs[i]; } return ret; }; function merge(obj, ext) { var count = 0; - for (var i in ext) if (ext.hasOwnProperty(i)) { + for (var i in ext) if (HOP(ext, i)) { obj[i] = ext[i]; count++; } @@ -150,7 +150,7 @@ var MAP = (function(){ } } else { - for (i in a) if (a.hasOwnProperty(i)) if (doit()) break; + for (i in a) if (HOP(a, i)) if (doit()) break; } return top.concat(ret); }; @@ -308,3 +308,7 @@ Dictionary.fromObject = function(obj) { dict._size = merge(dict._values, obj); return dict; }; + +function HOP(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} From c70d176f359c5640d3eb64a639958bec5fe45f7e Mon Sep 17 00:00:00 2001 From: kzc Date: Thu, 7 Apr 2016 09:57:30 -0400 Subject: [PATCH 46/51] Simplify member(name, array) implementation. --- lib/utils.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index c81ca71f..78c6dbf7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -59,10 +59,7 @@ function characters(str) { }; function member(name, array) { - for (var i = array.length; --i >= 0;) - if (array[i] == name) - return true; - return false; + return array.indexOf(name) >= 0; }; function find_if(func, array) { From 3907a5e3b2c40904f2cb333ede23d6d59bb48c13 Mon Sep 17 00:00:00 2001 From: kzc Date: Sun, 10 Apr 2016 15:41:38 -0400 Subject: [PATCH 47/51] Fix warnings for referenced non-hoisted functions. Fixes #1034 Also added `expect_warnings` functionality to test framework. --- lib/compress.js | 4 +- test/compress/issue-1034.js | 137 ++++++++++++++++++++++++++++++++++++ test/run-tests.js | 28 +++++++- 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 test/compress/issue-1034.js diff --git a/lib/compress.js b/lib/compress.js index d42f84d1..e47be97b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -801,7 +801,9 @@ merge(Compressor.prototype, { }; function extract_declarations_from_unreachable_code(compressor, stat, target) { - compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); + if (!(stat instanceof AST_Defun)) { + compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); + } stat.walk(new TreeWalker(function(node){ if (node instanceof AST_Definitions) { compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start); diff --git a/test/compress/issue-1034.js b/test/compress/issue-1034.js new file mode 100644 index 00000000..039af2ed --- /dev/null +++ b/test/compress/issue-1034.js @@ -0,0 +1,137 @@ +non_hoisted_function_after_return: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true + } + input: { + function foo(x) { + if (x) { + return bar(); + not_called1(); + } else { + return baz(); + not_called2(); + } + function bar() { return 7; } + return not_reached; + function UnusedFunction() {} + function baz() { return 8; } + } + } + expect: { + function foo(x) { + return x ? bar() : baz(); + function bar() { return 7 } + function baz() { return 8 } + } + } + expect_warnings: [ + 'WARN: Dropping unreachable code [test/compress/issue-1034.js:11,16]', + "WARN: Dropping unreachable code [test/compress/issue-1034.js:14,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:17,12]", + "WARN: Dropping unused function UnusedFunction [test/compress/issue-1034.js:18,21]" + ] +} + +non_hoisted_function_after_return_2a: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true, + collapse_vars: false + } + input: { + function foo(x) { + if (x) { + return bar(1); + var a = not_called(1); + } else { + return bar(2); + var b = not_called(2); + } + var c = bar(3); + function bar(x) { return 7 - x; } + function nope() {} + return b || c; + } + } + expect: { + // NOTE: Output is correct, but suboptimal. Not a regression. Can be improved in future. + // This output is run through non_hoisted_function_after_return_2b with same flags. + function foo(x) { + if (x) { + return bar(1); + } else { + return bar(2); + var b; + } + var c = bar(3); + function bar(x) { + return 7 - x; + } + return b || c; + } + } + expect_warnings: [ + "WARN: Dropping unreachable code [test/compress/issue-1034.js:48,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:48,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:51,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:51,16]", + "WARN: Dropping unused variable a [test/compress/issue-1034.js:48,20]", + "WARN: Dropping unused function nope [test/compress/issue-1034.js:55,21]" + ] +} + +non_hoisted_function_after_return_2b: { + options = { + hoist_funs: false, dead_code: true, conditionals: true, comparisons: true, + evaluate: true, booleans: true, loops: true, unused: true, keep_fargs: true, + if_return: true, join_vars: true, cascade: true, side_effects: true, + collapse_vars: false + } + input: { + // Note: output of test non_hoisted_function_after_return_2a run through compress again + function foo(x) { + if (x) { + return bar(1); + } else { + return bar(2); + var b; + } + var c = bar(3); + function bar(x) { + return 7 - x; + } + return b || c; + } + } + expect: { + // the output we would have liked to see from non_hoisted_function_after_return_2a + function foo(x) { + return bar(x ? 1 : 2); + function bar(x) { return 7 - x; } + } + } + expect_warnings: [ + // Notice that some warnings are repeated by multiple compress passes. + // Not a regression. There is room for improvement here. + // Warnings should be cached and only output if unique. + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:102,12]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:102,12]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:106,12]", + "WARN: Dropping unreachable code [test/compress/issue-1034.js:100,16]", + "WARN: Declarations in unreachable code! [test/compress/issue-1034.js:100,16]", + "WARN: Dropping unused variable b [test/compress/issue-1034.js:100,20]", + "WARN: Dropping unused variable c [test/compress/issue-1034.js:102,16]" + ] +} + diff --git a/test/run-tests.js b/test/run-tests.js index fcb1b375..b4333b7a 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -88,6 +88,14 @@ function run_compress_tests() { var options = U.defaults(test.options, { warnings: false }); + var warnings_emitted = []; + var original_warn_function = U.AST_Node.warn_function; + if (test.expect_warnings) { + U.AST_Node.warn_function = function(text) { + warnings_emitted.push("WARN: " + text); + }; + options.warnings = true; + } var cmp = new U.Compressor(options, true); var output_options = test.beautify || {}; var expect; @@ -117,6 +125,24 @@ function run_compress_tests() { failures++; failed_files[file] = 1; } + else if (test.expect_warnings) { + U.AST_Node.warn_function = original_warn_function; + var expected_warnings = make_code(test.expect_warnings, { + beautify: false, + quote_style: 2, // force double quote to match JSON + }); + var actual_warnings = JSON.stringify(warnings_emitted); + actual_warnings = actual_warnings.split(process.cwd() + "/").join(""); + if (expected_warnings != actual_warnings) { + log("!!! failed\n---INPUT---\n{input}\n---EXPECTED WARNINGS---\n{expected_warnings}\n---ACTUAL WARNINGS---\n{actual_warnings}\n\n", { + input: input_code, + expected_warnings: expected_warnings, + actual_warnings: actual_warnings, + }); + failures++; + failed_files[file] = 1; + } + } } var tests = parse_test(path.resolve(dir, file)); for (var i in tests) if (tests.hasOwnProperty(i)) { @@ -168,7 +194,7 @@ function parse_test(file) { } if (node instanceof U.AST_LabeledStatement) { assert.ok( - node.label.name == "input" || node.label.name == "expect" || node.label.name == "expect_exact", + ["input", "expect", "expect_exact", "expect_warnings"].indexOf(node.label.name) >= 0, tmpl("Unsupported label {name} [{line},{col}]", { name: node.label.name, line: node.label.start.line, From b5a7a231f7e9f09cc00e92c970d304d6071f7ac1 Mon Sep 17 00:00:00 2001 From: Mihai Bazon Date: Tue, 12 Apr 2016 14:15:14 +0300 Subject: [PATCH 48/51] Actually limit sequence length. Fix #1038 --- lib/ast.js | 7 +++++++ lib/compress.js | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 2a461834..42506cb2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -633,6 +633,13 @@ var AST_Seq = DEFNODE("Seq", "car cdr", { p = p.cdr; } }, + len: function() { + if (this.cdr instanceof AST_Seq) { + return this.cdr.len() + 1; + } else { + return 2; + } + }, _walk: function(visitor) { return visitor._visit(this, function(){ this.car._walk(visitor); diff --git a/lib/compress.js b/lib/compress.js index e47be97b..153e70fb 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -673,8 +673,12 @@ merge(Compressor.prototype, { seq = []; }; statements.forEach(function(stat){ - if (stat instanceof AST_SimpleStatement && seq.length < 2000) seq.push(stat.body); - else push_seq(), ret.push(stat); + if (stat instanceof AST_SimpleStatement && seqLength(seq) < 2000) { + seq.push(stat.body); + } else { + push_seq(); + ret.push(stat); + } }); push_seq(); ret = sequencesize_2(ret, compressor); @@ -682,6 +686,18 @@ merge(Compressor.prototype, { return ret; }; + function seqLength(a) { + for (var len = 0, i = 0; i < a.length; ++i) { + var stat = a[i]; + if (stat instanceof AST_Seq) { + len += stat.len(); + } else { + len++; + } + } + return len; + }; + function sequencesize_2(statements, compressor) { function cons_seq(right) { ret.pop(); From 187a0caf9d2b283387b57b71dea6aa133b266638 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 12 Apr 2016 20:08:09 +0200 Subject: [PATCH 49/51] Add base54.reset() to compress tests Without this reset, char counts bleed to next tests. One test had a bad expect clause. --- test/compress/issue-976.js | 2 +- test/run-tests.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/compress/issue-976.js b/test/compress/issue-976.js index dea30705..06e11f40 100644 --- a/test/compress/issue-976.js +++ b/test/compress/issue-976.js @@ -84,5 +84,5 @@ eval_mangle: { return a + eval('c'); } } - expect_exact: 'function f1(n,c,e,a,o){return n("c")+c}function f2(a,b,c,d,e){return a+eval("c")}function f3(a,eval,c,d,e){return a+eval("c")}' + expect_exact: 'function f1(n,c,e,a,f){return n("c")+c}function f2(a,b,c,d,e){return a+eval("c")}function f3(a,eval,c,d,e){return a+eval("c")}' } diff --git a/test/run-tests.js b/test/run-tests.js index b4333b7a..e9ce3bb2 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -85,6 +85,7 @@ function run_compress_tests() { log_start_file(file); function test_case(test) { log_test(test.name); + U.base54.reset(); var options = U.defaults(test.options, { warnings: false }); From 4b4528ee0552edb7ba1d24dad89e29880065e1c0 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 12 Apr 2016 20:30:44 +0200 Subject: [PATCH 50/51] Prevent endless recursion when evaluating self-referencing consts Fix #1041 --- lib/compress.js | 12 ++++++++++-- test/compress/issue-1041.js | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 test/compress/issue-1041.js diff --git a/lib/compress.js b/lib/compress.js index 153e70fb..3e33c1b4 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1030,8 +1030,16 @@ merge(Compressor.prototype, { : ev(this.alternative, compressor); }); def(AST_SymbolRef, function(compressor){ - var d = this.definition(); - if (d && d.constant && d.init) return ev(d.init, compressor); + if (this._evaluating) throw def; + this._evaluating = true; + try { + var d = this.definition(); + if (d && d.constant && d.init) { + return ev(d.init, compressor); + } + } finally { + this._evaluating = false; + } throw def; }); def(AST_Dot, function(compressor){ diff --git a/test/compress/issue-1041.js b/test/compress/issue-1041.js new file mode 100644 index 00000000..9dd176fd --- /dev/null +++ b/test/compress/issue-1041.js @@ -0,0 +1,39 @@ +const_declaration: { + options = { + evaluate: true + }; + + input: { + const goog = goog || {}; + } + expect: { + const goog = goog || {}; + } +} + +const_pragma: { + options = { + evaluate: true + }; + + input: { + /** @const */ var goog = goog || {}; + } + expect: { + var goog = goog || {}; + } +} + +// for completeness' sake +not_const: { + options = { + evaluate: true + }; + + input: { + var goog = goog || {}; + } + expect: { + var goog = goog || {}; + } +} From e4fa4b109a0db5691d91b3dfba0eac41ac21c0ef Mon Sep 17 00:00:00 2001 From: kzc Date: Fri, 15 Apr 2016 19:58:46 -0400 Subject: [PATCH 51/51] Parse comments without recursion to avoid RangeError. Fixes #993 --- lib/parse.js | 67 +++++++++++++++------------ test/mocha/huge-number-of-comments.js | 19 ++++++++ 2 files changed, 57 insertions(+), 29 deletions(-) create mode 100644 test/mocha/huge-number-of-comments.js diff --git a/lib/parse.js b/lib/parse.js index f16d092b..18d071f3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -421,7 +421,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { S.col = S.tokcol + (S.pos - S.tokpos); S.comments_before.push(token(type, ret, true)); S.regex_allowed = regex_allowed; - return next_token(); + return next_token; }; var skip_multiline_comment = with_eof_error("Unterminated multiline comment", function(){ @@ -439,7 +439,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { S.comments_before.push(token("comment2", text, true)); S.regex_allowed = regex_allowed; S.newline_before = nlb; - return next_token(); + return next_token; }); function read_name() { @@ -548,36 +548,45 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { function next_token(force_regexp) { if (force_regexp != null) return read_regexp(force_regexp); - skip_whitespace(); - start_token(); - if (html5_comments) { - if (looking_at("") && S.newline_before) { + forward(3); + skip_line_comment("comment4"); + continue; + } } - if (looking_at("-->") && S.newline_before) { - forward(3); - return skip_line_comment("comment4"); + var ch = peek(); + if (!ch) return token("eof"); + var code = ch.charCodeAt(0); + switch (code) { + case 34: case 39: return read_string(ch); + case 46: return handle_dot(); + case 47: { + var tok = handle_slash(); + if (tok === next_token) continue; + return tok; + } } - } - var ch = peek(); - if (!ch) return token("eof"); - var code = ch.charCodeAt(0); - switch (code) { - case 34: case 39: return read_string(ch); - case 46: return handle_dot(); - case 47: return handle_slash(); - } - if (is_digit(code)) return read_num(); - if (PUNC_CHARS(ch)) return token("punc", next()); - if (OPERATOR_CHARS(ch)) return read_operator(); - if (code == 92 || is_identifier_start(code)) return read_word(); - - if (shebang) { - if (S.pos == 0 && looking_at("#!")) { - forward(2); - return skip_line_comment("comment5"); + if (is_digit(code)) return read_num(); + if (PUNC_CHARS(ch)) return token("punc", next()); + if (OPERATOR_CHARS(ch)) return read_operator(); + if (code == 92 || is_identifier_start(code)) return read_word(); + if (shebang) { + if (S.pos == 0 && looking_at("#!")) { + forward(2); + skip_line_comment("comment5"); + continue; + } } + break; } parse_error("Unexpected character '" + ch + "'"); }; diff --git a/test/mocha/huge-number-of-comments.js b/test/mocha/huge-number-of-comments.js new file mode 100644 index 00000000..3b90bc0e --- /dev/null +++ b/test/mocha/huge-number-of-comments.js @@ -0,0 +1,19 @@ +var Uglify = require('../../'); +var assert = require("assert"); + +describe("Huge number of comments.", function() { + it("Should parse and compress code with thousands of consecutive comments", function() { + var js = 'function lots_of_comments(x) { return 7 -'; + var i; + for (i = 1; i <= 5000; ++i) { js += "// " + i + "\n"; } + for (; i <= 10000; ++i) { js += "/* " + i + " */ /**/"; } + js += "x; }"; + var result = Uglify.minify(js, { + fromString: true, + mangle: false, + compress: {} + }); + assert.strictEqual(result.code, "function lots_of_comments(x){return 7-x}"); + }); +}); +