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 001/105] 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 002/105] 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 003/105] 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 004/105] 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 005/105] 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 006/105] 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 007/105] 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 008/105] 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 009/105] 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 010/105] 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 011/105] 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 012/105] 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 013/105] 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 014/105] 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 015/105] 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 016/105] 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 017/105] 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 018/105] 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 019/105] 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 020/105] 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 021/105] 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 022/105] 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 023/105] 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 024/105] 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 025/105] 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 026/105] 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 027/105] 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 028/105] 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 029/105] 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 030/105] 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 031/105] 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 032/105] 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 033/105] 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 034/105] 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 035/105] 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 036/105] 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 037/105] #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 038/105] 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 aaa8f25bf0dc30ac2d40900887c8f523e47582dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 03:03:38 +0000 Subject: [PATCH 039/105] Starting destructuring. --- lib/ast.js | 90 +++++++++++++++++++++++++++++++++++++++- lib/compress.js | 34 ++++++++++----- lib/output.js | 11 +++++ lib/parse.js | 102 ++++++++++++++++++++++++++++++++------------- lib/scope.js | 30 +++++++++++++- test/parser.js | 103 ++++++++++++++++++++++++++++++++++++++++++++++ test/run-tests.js | 4 ++ 7 files changed, 332 insertions(+), 42 deletions(-) create mode 100644 test/parser.js diff --git a/lib/ast.js b/lib/ast.js index 0ac14dc9..1acbc4a9 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -359,13 +359,73 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); +var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { + $documentation: "A set of arrow function parameters or a sequence expression. This is used because when the parser sees a \"(\" it could be the start of a seq, or the start of a parameter list of an arrow function.", + $propdoc: { + expressions: "[AST_Expression|AST_Destructuring*] array of expressions or argument names or destructurings." + }, + as_params: function (croak) { + // We don't want anything which doesn't belong in a destructuring + var root = this; + return this.expressions.map(function to_fun_args(ex) { + if (ex instanceof AST_Object) { + if (ex.properties.length == 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + return new AST_Destructuring({ + start: ex.start, + end: ex.end, + is_array: false, + names: ex.properties.map(to_fun_args) + }); + } else if (ex instanceof AST_ObjectSymbol) { + return new AST_SymbolFunarg({ + name: ex.symbol.name, + start: ex.start, + end: ex.end + }); + } else if (ex instanceof AST_SymbolRef) { + return new AST_SymbolFunarg({ + name: ex.name, + start: ex.start, + end: ex.end + }); + } else if (ex instanceof AST_Array) { + if (ex.elements.length === 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + return new AST_Destructuring({ + start: ex.start, + end: ex.end, + is_array: true, + names: ex.elements.map(to_fun_args) + }); + } else { + console.log(ex.__proto__.TYPE) + croak("Invalid function parameter", ex.start.line, ex.start.col); + } + }); + }, + as_expr: function (croak) { + return AST_Seq.from_array(this.expressions); + } +}); + var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { $documentation: "Base class for functions", $propdoc: { name: "[AST_SymbolDeclaration?] the name of this function", - argnames: "[AST_SymbolFunarg*] array of function arguments", + argnames: "[AST_SymbolFunarg|AST_Destructuring*] array of function arguments or destructurings", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, + args_as_names: function () { + var out = []; + this.walk(new TreeWalker(function (parm) { + var that = this; + if (parm instanceof AST_SymbolFunarg) { + out.push(parm); + } + })); + return out; + }, _walk: function(visitor) { return visitor._visit(this, function(){ if (this.name) this.name._walk(visitor); @@ -385,10 +445,26 @@ var AST_Function = DEFNODE("Function", null, { $documentation: "A function expression" }, AST_Lambda); +var AST_Arrow = DEFNODE("Arrow", null, { + $documentation: "An ES6 Arrow function ((a) => b)" +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); +/* -----[ DESTRUCTURING ]----- */ +var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { + $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.names.forEach(function(name){ + name._walk(visitor); + }); + }); + } +}); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { @@ -774,6 +850,18 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { } }, AST_ObjectProperty); +var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { + $propdoc: { + symbol: "[AST_SymbolRef] what symbol it is" + }, + $documentation: "A symbol in an object", + _walk: function (visitor) { + return visitor._visit(this, function(){ + this.symbol._walk(visitor); + }); + } +}, AST_ObjectProperty); + var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { $documentation: "An object setter property", }, AST_ObjectProperty); diff --git a/lib/compress.js b/lib/compress.js index f49486a0..3ae18255 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -410,6 +410,7 @@ merge(Compressor.prototype, { } function make_arguments_names_list(func) { return func.argnames.map(function(sym){ + // TODO not sure what to do here with destructuring return make_node(AST_String, sym, { value: sym.name }); }); } @@ -1300,17 +1301,26 @@ merge(Compressor.prototype, { if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { if (!compressor.option("keep_fargs")) { for (var a = node.argnames, i = a.length; --i >= 0;) { - var sym = a[i]; - if (sym.unreferenced()) { - a.pop(); - compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { - name : sym.name, - file : sym.start.file, - line : sym.start.line, - col : sym.start.col - }); + if (a[i] instanceof AST_Destructuring) { + // Do not drop destructuring arguments. + // They constitute a type assertion, so dropping + // them would stop that TypeError which would happen + // if someone called it with an incorrectly formatted + // parameter. + break; + } else { + var sym = a[i]; + if (sym.unreferenced()) { + a.pop(); + compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { + name : sym.name, + file : sym.start.file, + line : sym.start.line, + col : sym.start.col + }); + } + else break; } - else break; } } } @@ -1478,9 +1488,10 @@ merge(Compressor.prototype, { // collect only vars which don't show up in self's arguments list var defs = []; vars.each(function(def, name){ + // TODO test this too if (self instanceof AST_Lambda && find_if(function(x){ return x.name == def.name.name }, - self.argnames)) { + self.args_as_names())) { vars.del(name); } else { def = def.clone(); @@ -2011,6 +2022,7 @@ merge(Compressor.prototype, { if (ex !== ast) throw ex; }; if (!fun) return self; + // TODO does this work with destructuring? Test it. var args = fun.argnames.map(function(arg, i){ return make_node(AST_String, self.args[i], { value: arg.print_to_string() diff --git a/lib/output.js b/lib/output.js index a8c45a0f..374fa7a7 100644 --- a/lib/output.js +++ b/lib/output.js @@ -629,6 +629,17 @@ function OutputStream(options) { output.print_string(self.value, self.quote); output.semicolon(); }); + + DEFPRINT(AST_Destructuring, function (self, output) { + output.print(self.is_array ? "[" : "{"); + var first = true; + self.names.forEach(function (name) { + if (first) first = false; else { output.comma(); output.space(); } + name.print(output); + }) + output.print(self.is_array ? "]" : "}"); + }) + DEFPRINT(AST_Debugger, function(self, output){ output.print("debugger"); output.semicolon(); diff --git a/lib/parse.js b/lib/parse.js index f1495153..c804a385 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -660,6 +660,7 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, + in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -973,35 +974,58 @@ function parse($TEXT, options) { }; var function_ = function(ctor) { + var start = S.token + var in_statement = ctor === AST_Defun; var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; if (in_statement && !name) unexpected(); - expect("("); + + var args = params_or_seq_().as_params(croak); + var body = _function_body(); return new ctor({ - name: name, - argnames: (function(first, a){ - while (!is("punc", ")")) { - if (first) first = false; else expect(","); - a.push(as_symbol(AST_SymbolFunarg)); - } - next(); - return a; - })(true, []), - body: (function(loop, labels){ - ++S.in_function; - S.in_directives = true; - S.in_loop = 0; - S.labels = []; - var a = block_(); - --S.in_function; - S.in_loop = loop; - S.labels = labels; - return a; - })(S.in_loop, S.labels) + start : args.start, + end : body.end, + name : name, + argnames: args, + body : body }); }; + function params_or_seq_() { + var start = S.token + expect("("); + var first = true; + var a = []; + S.in_parameters = true; + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + a.push(expression(false)); + } + S.in_parameters = false; + var end = S.token + next(); + return new AST_ArrowParametersOrSeq({ + start: start, + end: end, + expressions: a + }); + } + + function _function_body() { + var loop = S.in_loop; + var labels = S.labels; + ++S.in_function; + S.in_directives = true; + S.in_loop = 0; + S.labels = []; + var a = block_(); + --S.in_function; + S.in_loop = loop; + S.labels = labels; + return a; + } + function if_() { var cond = parenthesised(), body = statement(), belse = null; if (is("keyword", "else")) { @@ -1247,6 +1271,7 @@ function parse($TEXT, options) { }); var object_ = embed_tokens(function() { + var start = S.token; expect("{"); var first = true, a = []; while (!is("punc", "}")) { @@ -1277,14 +1302,33 @@ function parse($TEXT, options) { continue; } } - expect(":"); - a.push(new AST_ObjectKeyVal({ - start : start, - quote : start.quote, - key : name, - value : expression(false), - end : prev() - })); + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + if (!S.in_parameters) { + croak("Invalid syntax", S.token.line, S.token.col); + } + a.push(new AST_ObjectSymbol({ + start: start, + end: start, + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + if (S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); + } } next(); return new AST_Object({ properties: a }); diff --git a/lib/scope.js b/lib/scope.js index 5ab775a3..dee748f9 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -50,6 +50,7 @@ function SymbolDef(scope, index, orig) { this.references = []; this.global = false; this.mangled_name = null; + this.object_destructuring_arg = false; this.undeclared = false; this.constant = false; this.index = index; @@ -60,6 +61,7 @@ SymbolDef.prototype = { if (!options) options = {}; return (this.global && !options.toplevel) + || this.object_destructuring_arg || this.undeclared || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) || (options.keep_fnames @@ -96,6 +98,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var defun = null; var last_var_had_const_pragma = false; var nesting = 0; + var object_destructuring_arg = false; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -106,6 +109,12 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ scope = save_scope; return true; } + if (node instanceof AST_Destructuring && node.is_array === false) { + object_destructuring_arg = true; // These don't nest + descend(); + object_destructuring_arg = false; + return true; + } if (node instanceof AST_Scope) { node.init_scope_vars(nesting); var save_scope = node.parent_scope = scope; @@ -137,10 +146,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ if (node instanceof AST_Symbol) { node.scope = scope; } - if (node instanceof AST_Label) { + if (node instanceof AST_Label) { node.thedef = node; node.references = []; } + if (node instanceof AST_SymbolFunarg) { + node.object_destructuring_arg = object_destructuring_arg; + defun.def_variable(node); + } if (node instanceof AST_SymbolLambda) { defun.def_function(node); } @@ -211,6 +224,10 @@ 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; } @@ -229,6 +246,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ }); AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ + this.directives = []; // contains the directives defined in this scope, i.e. "use strict" this.variables = new Dictionary(); // map name to AST_SymbolVar (variables defined in this scope; includes functions) this.functions = new Dictionary(); // map name to AST_SymbolDefun (functions defined in this scope) this.uses_with = false; // will be set to true if this or some nested scope uses the `with` statement @@ -239,6 +257,10 @@ AST_Scope.DEFMETHOD("init_scope_vars", function(nesting){ this.nesting = nesting; // the nesting level of this scope (0 means toplevel) }); +AST_Scope.DEFMETHOD("strict", function(){ + return this.has_directive("use strict"); +}); + AST_Lambda.DEFMETHOD("init_scope_vars", function(){ AST_Scope.prototype.init_scope_vars.apply(this, arguments); this.uses_arguments = false; @@ -266,6 +288,11 @@ AST_Scope.DEFMETHOD("find_variable", function(name){ || (this.parent_scope && this.parent_scope.find_variable(name)); }); +AST_Scope.DEFMETHOD("has_directive", function(value){ + return this.parent_scope && this.parent_scope.has_directive(value) + || (this.directives.indexOf(value) >= 0 ? this : null); +}); + AST_Scope.DEFMETHOD("def_function", function(symbol){ this.functions.set(symbol.name, this.def_variable(symbol)); }); @@ -275,6 +302,7 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ if (!this.variables.has(symbol.name)) { def = new SymbolDef(this, this.variables.size(), symbol); this.variables.set(symbol.name, def); + def.object_destructuring_arg = symbol.object_destructuring_arg; def.global = !this.parent_scope; } else { def = this.variables.get(symbol.name); diff --git a/test/parser.js b/test/parser.js new file mode 100644 index 00000000..b512f1ca --- /dev/null +++ b/test/parser.js @@ -0,0 +1,103 @@ + +var UglifyJS = require(".."); +var ok = require('assert'); + +module.exports = function () { + console.log("--- Parser tests"); + + // Destructuring arguments + + // Function argument nodes are correct + function get_args(args) { + return args.map(function (arg) { + return [arg.TYPE, arg.name]; + }); + } + + // Destructurings as arguments + var destr_fun1 = UglifyJS.parse('(function ({a, b}) {})').body[0].body; + var destr_fun2 = UglifyJS.parse('(function ([a, [b]]) {})').body[0].body; + + ok.equal(destr_fun1.argnames.length, 1); + ok.equal(destr_fun2.argnames.length, 1); + + var destruct1 = destr_fun1.argnames[0]; + var destruct2 = destr_fun2.argnames[0]; + + ok(destruct1 instanceof UglifyJS.AST_Destructuring); + ok(destruct2 instanceof UglifyJS.AST_Destructuring); + ok(destruct2.names[1] instanceof UglifyJS.AST_Destructuring); + + ok.equal(destruct1.start.value, '{'); + ok.equal(destruct1.end.value, '}'); + ok.equal(destruct2.start.value, '['); + ok.equal(destruct2.end.value, ']'); + + ok.equal(destruct1.is_array, false); + ok.equal(destruct2.is_array, true); + + var aAndB = [ + ['SymbolFunarg', 'a'], + ['SymbolFunarg', 'b'] + ]; + + ok.deepEqual( + [ + destruct1.names[0].TYPE, + destruct1.names[0].name], + aAndB[0]); + + ok.deepEqual( + [ + destruct2.names[1].names[0].TYPE, + destruct2.names[1].names[0].name + ], + aAndB[1]); + + ok.deepEqual( + get_args(destr_fun1.args_as_names()), + aAndB) + ok.deepEqual( + get_args(destr_fun2.args_as_names()), + aAndB) + + // Making sure we don't accidentally accept things which + // Aren't argument destructurings + + ok.throws(function () { + UglifyJS.parse('(function ([]) {})'); + }, /Invalid destructuring function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function ( { a, [ b ] } ) { })') + }); + + ok.throws(function () { + UglifyJS.parse('(function (1) { })'); + }, /Invalid function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function (this) { })'); + }); + + ok.throws(function () { + UglifyJS.parse('(function ([1]) { })'); + }, /Invalid function parameter/); + + ok.throws(function () { + UglifyJS.parse('(function [a] { })'); + }); + + ok.throws(function () { + // Note: this *is* a valid destructuring, but before we implement + // destructuring (right now it's only destructuring *arguments*), + // this won't do. + UglifyJS.parse('[{a}]'); + }); +} + +// Run standalone +if (module.parent === null) { + module.exports(); +} + diff --git a/test/run-tests.js b/test/run-tests.js index fcb1b375..9a31d148 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -28,6 +28,10 @@ run_ast_conversion_tests({ iterations: 1000 }); +var run_parser_tests = require('./parser.js'); + +run_parser_tests(); + /* -----[ utils ]----- */ function tmpl() { From 6645da0ea2f9aca512d38386a5acf7207f09094b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 10:23:17 +0000 Subject: [PATCH 040/105] remove trace statement --- lib/ast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 1acbc4a9..0d3c79f6 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -399,7 +399,6 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { names: ex.elements.map(to_fun_args) }); } else { - console.log(ex.__proto__.TYPE) croak("Invalid function parameter", ex.start.line, ex.start.col); } }); From 708001d2f586797869f2738c3d7250369b7ddf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 10:44:41 +0000 Subject: [PATCH 041/105] do not support destructuring arguments and ngInject --- lib/compress.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 3ae18255..ea389cc1 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -409,9 +409,16 @@ merge(Compressor.prototype, { return /@ngInject/.test(comment.value); } function make_arguments_names_list(func) { + var foundDestructuring = false; return func.argnames.map(function(sym){ - // TODO not sure what to do here with destructuring + if (sym instanceof AST_Destructuring) { + compressor.warn("Function with destructuring arguments marked with @ngInject [{file}:{line},{col}]", token); + foundDestructuring = true; + } + if (foundDestructuring) { return null; } return make_node(AST_String, sym, { value: sym.name }); + }).filter(function (name) { + return name !== null; }); } function make_array(orig, elements) { From 3f87afdb2c23be43a1616293f4fa9aebc3a52eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 13:27:30 +0000 Subject: [PATCH 042/105] test that names used in destructurings don't get hoisted --- lib/compress.js | 1 - test/compress/hoist.js | 67 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 test/compress/hoist.js diff --git a/lib/compress.js b/lib/compress.js index ea389cc1..d4fa5200 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1495,7 +1495,6 @@ merge(Compressor.prototype, { // collect only vars which don't show up in self's arguments list var defs = []; vars.each(function(def, name){ - // TODO test this too if (self instanceof AST_Lambda && find_if(function(x){ return x.name == def.name.name }, self.args_as_names())) { diff --git a/test/compress/hoist.js b/test/compress/hoist.js new file mode 100644 index 00000000..2b359fc5 --- /dev/null +++ b/test/compress/hoist.js @@ -0,0 +1,67 @@ + +hoist_vars: { + options = { + hoist_vars: true + } + input: { + function a() { + bar(); + var var1; + var var2; + } + function b(anArg) { + bar(); + var var1; + var anArg; + } + } + expect: { + function a() { + var var1, var2; // Vars go up and are joined + bar(); + } + function b(anArg) { + var var1; + bar(); + // But vars named like arguments go away! + } + } +} + +hoist_funs: { + options = { + hoist_funs: true + } + input: { + function a() { + bar(); + function foo() {} + } + } + expect: { + function a() { + function foo() {} // Funs go up + bar(); + } + } +} + +hoist_no_destructurings: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + function a([anArg]) { + bar(); + var var1; + var anArg; // Because anArg is already declared, this goes away! + } + } + expect: { + function a([anArg]) { + var var1; + bar(); + } + } +} From 8ca033e7f2fb3cdec0d3385cbc3538ff4a5c0b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 20:08:06 +0000 Subject: [PATCH 043/105] Add a test to verify that destructuring arguments work with #203 code --- lib/compress.js | 1 - test/compress/issue-203.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-203.js diff --git a/lib/compress.js b/lib/compress.js index d4fa5200..e5782545 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2028,7 +2028,6 @@ merge(Compressor.prototype, { if (ex !== ast) throw ex; }; if (!fun) return self; - // TODO does this work with destructuring? Test it. var args = fun.argnames.map(function(arg, i){ return make_node(AST_String, self.args[i], { value: arg.print_to_string() diff --git a/test/compress/issue-203.js b/test/compress/issue-203.js new file mode 100644 index 00000000..d894c586 --- /dev/null +++ b/test/compress/issue-203.js @@ -0,0 +1,30 @@ + +compress_new_function: { + options = { + unsafe: true + } + input: { + new Function("aa, bb", 'return aa;'); + } + expect: { + Function("a", "b", "return a"); + } +} + +compress_new_function_with_destruct: { + options = { + unsafe: true + } + input: { + new Function("aa, [bb]", 'return aa;'); + new Function("aa, {bb}", 'return aa;'); + new Function("[[aa]], [{bb}]", 'return aa;'); + } + expect: { + Function("a", "[b]", "return a"); + Function("a", "{bb}", "return a"); + Function("[[a]]", "[{bb}]", 'return a'); + } +} + + From e4b87bfaceed1cebb40965599364452a8e0b1a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 13 Apr 2015 01:25:46 +0100 Subject: [PATCH 044/105] for...of --- lib/ast.js | 4 ++++ lib/output.js | 6 +++++- lib/parse.js | 24 +++++++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0d3c79f6..69a3affc 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -263,6 +263,10 @@ var AST_ForIn = DEFNODE("ForIn", "init name object", { } }, AST_IterationStatement); +var AST_ForOf = DEFNODE("ForOf", null, { + $documentation: "A `for ... of` statement", +}, AST_ForIn); + var AST_With = DEFNODE("With", "expression", { $documentation: "A `with` statement", $propdoc: { diff --git a/lib/output.js b/lib/output.js index 374fa7a7..4c5f50e9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -750,7 +750,11 @@ function OutputStream(options) { output.with_parens(function(){ self.init.print(output); output.space(); - output.print("in"); + if (self instanceof AST_ForOf) { + output.print("of"); + } else { + output.print("in"); + } output.space(); self.object.print(output); }); diff --git a/lib/parse.js b/lib/parse.js index c804a385..747cedb2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -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 = 'break case catch const continue debugger default delete do else finally for function if in of 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' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -937,11 +937,17 @@ function parse($TEXT, options) { init = is("keyword", "var") ? (next(), var_(true)) : expression(true, true); - if (is("operator", "in")) { + var is_in = is("operator", "in"); + var is_of = is("keyword", "of"); + if (is_in || is_of) { if (init instanceof AST_Var && init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); next(); - return for_in(init); + if (is_in) { + return for_in(init); + } else { + return for_of(init); + } } } return regular_for(init); @@ -961,6 +967,18 @@ function parse($TEXT, options) { }); }; + function for_of(init) { + var lhs = init instanceof AST_Var ? init.definitions[0].name : null; + var obj = expression(true); + expect(")"); + return new AST_ForOf({ + init : init, + name : lhs, + object : obj, + body : in_loop(statement) + }); + }; + function for_in(init) { var lhs = init instanceof AST_Var ? init.definitions[0].name : null; var obj = expression(true); From aa127457d533a87ce34a39d5c262547d886438f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 11 Jan 2015 20:07:19 +0000 Subject: [PATCH 045/105] Adding arrow functions --- lib/ast.js | 2 +- lib/compress.js | 3 +++ lib/output.js | 28 ++++++++++++++++++++ lib/parse.js | 56 ++++++++++++++++++++++++++++++++++++++++ lib/transform.js | 6 ++++- test/compress/harmony.js | 37 ++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 test/compress/harmony.js diff --git a/lib/ast.js b/lib/ast.js index 69a3affc..19099ab0 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -143,7 +143,7 @@ var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { }, AST_Statement); function walk_body(node, visitor) { - if (node.body instanceof AST_Statement) { + if (node.body instanceof AST_Node) { node.body._walk(visitor); } else node.body.forEach(function(stat){ diff --git a/lib/compress.js b/lib/compress.js index e5782545..aa54db1b 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1229,6 +1229,7 @@ merge(Compressor.prototype, { }); OPT(AST_Block, function(self, compressor){ + if (self.body instanceof AST_Node) { return self; } self.body = tighten_body(self.body, compressor); return self; }); @@ -1437,6 +1438,8 @@ merge(Compressor.prototype, { if (compressor.has_directive("use asm")) return self; var hoist_funs = compressor.option("hoist_funs"); var hoist_vars = compressor.option("hoist_vars"); + var self = this; + if (!(self.body instanceof Array)) { return self; } // Hoisting makes no sense in an arrow func if (hoist_funs || hoist_vars) { var dirs = []; var hoisted = []; diff --git a/lib/output.js b/lib/output.js index 4c5f50e9..d2d6cedb 100644 --- a/lib/output.js +++ b/lib/output.js @@ -794,6 +794,34 @@ function OutputStream(options) { self._do_print(output); }); + AST_Arrow.DEFMETHOD("_do_print", function(output){ + var self = this; + var parent = output.parent(); + var needs_parens = parent instanceof AST_Binary || + parent instanceof AST_Unary || + parent instanceof AST_Call; + if (needs_parens) { output.print("(") } + if (self.argnames.length === 1 && self.argnames[0] instanceof AST_Symbol) { + self.argnames[0].print(output); + } else { + output.with_parens(function(){ + self.argnames.forEach(function(arg, i){ + if (i) output.comma(); + arg.print(output); + }); + }); + } + output.space(); + output.print('=>'); + output.space(); + if (self.body instanceof AST_Node) { + this.body.print(output); + } else { + print_bracketed(this.body, output); + } + if (needs_parens) { output.print(")") } + }); + /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ output.print(kind); diff --git a/lib/parse.js b/lib/parse.js index 747cedb2..f892a89d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -518,6 +518,16 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { return S.regex_allowed ? read_regexp("") : read_operator("/"); }; + function handle_eq_sign() { + next(); + if (peek() === ">") { + next(); + return token("arrow", "=>"); + } else { + return read_operator("="); + } + }; + function handle_dot() { next(); return is_digit(peek().charCodeAt(0)) @@ -567,6 +577,7 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { case 34: case 39: return read_string(ch); case 46: return handle_dot(); case 47: return handle_slash(); + case 61: return handle_eq_sign(); } if (is_digit(code)) return read_num(); if (PUNC_CHARS(ch)) return token("punc", next()); @@ -991,6 +1002,41 @@ function parse($TEXT, options) { }); }; + var arrow_function = function(args) { + expect_token("arrow", "=>"); + + if (args instanceof AST_SymbolRef) { + args = [args]; + } else if (args instanceof AST_Seq) { + args = args.to_array(); + } else if (args instanceof AST_Node) { + croak("Invalid syntax", args.start.line, args.start.col); + } + + for (var i = 0; i < args.length; i++) { + if (!(args[i] instanceof AST_SymbolRef)) { + croak("Invalid parameter for an arrow function", args[i].start.line, args[i].start.col); + } + + args[i] = new AST_SymbolFunarg({ + name: args[i].name, + start: args[i].start, + end: args[i].end + }) + } + + return new AST_Arrow({ + argnames: args, + body: (function(){ + if (is("punc", "{")) { + return _function_body(); + } else { + return expression(true); + } + })() + }); + }; + var function_ = function(ctor) { var start = S.token @@ -1509,6 +1555,13 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + + if (start.value == "(" && peek().value == ")") { + next(); // ( + next(); // ) + return arrow_function([]); + } + var left = maybe_conditional(no_in), val = S.token.value; if (is("operator") && ASSIGNMENT(val)) { if (is_assignable(left)) { @@ -1523,6 +1576,9 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } + if (is("arrow")) { + return arrow_function(left) + } return left; }; diff --git a/lib/transform.js b/lib/transform.js index 3018e8ff..0087a0a8 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -166,7 +166,11 @@ TreeTransformer.prototype = new TreeWalker; _(AST_Lambda, function(self, tw){ if (self.name) self.name = self.name.transform(tw); self.argnames = do_list(self.argnames, tw); - self.body = do_list(self.body, tw); + if (self.body instanceof AST_Node) { + self.body = self.body.transform(tw); + } else { + self.body = do_list(self.body, tw); + } }); _(AST_Call, function(self, tw){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js new file mode 100644 index 00000000..3c07c49e --- /dev/null +++ b/test/compress/harmony.js @@ -0,0 +1,37 @@ +arrow_functions: { + input: { + (a) => b; // 1 args + (a, b) => c; // n args + () => b; // 0 args + (a) => (b) => c; // func returns func returns func + (a) => ((b) => c); // So these parens are dropped + () => (b,c) => d; // func returns func returns func + a=>{return b;} + a => 'lel'; // Dropping the parens + } + expect_exact: "a=>b;(a,b)=>c;()=>b;a=>b=>c;a=>b=>c;()=>(b,c)=>d;a=>{return b};a=>\"lel\";" +} + +arrow_function_parens: { + input: { + something && (() => {}); + } + expect_exact: "something&&(()=>{});" +} +arrow_function_parens_2: { + input: { + (() => null)(); + } + expect_exact: "(()=>null)();" +} + +regression_arrow_functions_and_hoist: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + (a) => b; + } + expect_exact: "a=>b;" +} From df95318e8b918402eaa2838c62b6a0151219a1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 4 Aug 2015 00:14:18 +0100 Subject: [PATCH 046/105] => with destructuring arguments. Requires a lot of parser changes --- lib/ast.js | 5 + lib/parse.js | 218 +++++++++++++++++++++------------------ test/compress/harmony.js | 62 +++++++++++ test/parser.js | 6 ++ 4 files changed, 193 insertions(+), 98 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 19099ab0..3baa63cf 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -387,6 +387,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end }); + } else if (ex instanceof AST_Destructuring) { + if (ex.names.length == 0) + croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); + ex.names = ex.names.map(to_fun_args); + return ex; } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, diff --git a/lib/parse.js b/lib/parse.js index f892a89d..3e6c826c 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1005,35 +1005,17 @@ function parse($TEXT, options) { var arrow_function = function(args) { expect_token("arrow", "=>"); - if (args instanceof AST_SymbolRef) { - args = [args]; - } else if (args instanceof AST_Seq) { - args = args.to_array(); - } else if (args instanceof AST_Node) { - croak("Invalid syntax", args.start.line, args.start.col); - } + var argnames = args.as_params(croak); - for (var i = 0; i < args.length; i++) { - if (!(args[i] instanceof AST_SymbolRef)) { - croak("Invalid parameter for an arrow function", args[i].start.line, args[i].start.col); - } - - args[i] = new AST_SymbolFunarg({ - name: args[i].name, - start: args[i].start, - end: args[i].end - }) - } + var body = is("punc", "{") ? + _function_body(true) : + _function_body(false); return new AST_Arrow({ - argnames: args, - body: (function(){ - if (is("punc", "{")) { - return _function_body(); - } else { - return expression(true); - } - })() + start : args.start, + end : body.end, + argnames : argnames, + body : body }); }; @@ -1046,7 +1028,7 @@ function parse($TEXT, options) { unexpected(); var args = params_or_seq_().as_params(croak); - var body = _function_body(); + var body = _function_body(true); return new ctor({ start : args.start, end : body.end, @@ -1076,14 +1058,18 @@ function parse($TEXT, options) { }); } - function _function_body() { + function _function_body(block) { var loop = S.in_loop; var labels = S.labels; ++S.in_function; - S.in_directives = true; + if (block) + S.in_directives = true; S.in_loop = 0; S.labels = []; - var a = block_(); + if (block) + var a = block_(); + else + var a = expression(false); --S.in_function; S.in_loop = loop; S.labels = labels; @@ -1286,16 +1272,17 @@ function parse($TEXT, options) { if (is("punc")) { switch (start.value) { case "(": - next(); - var ex = expression(true); + var ex = params_or_seq_(); ex.start = start; ex.end = S.token; - expect(")"); - return subscripts(ex, allow_calls); + if (is("arrow", "=>")) { + return arrow_function(ex); + } + return subscripts(ex.as_expr(croak), allow_calls); case "[": return subscripts(array_(), allow_calls); case "{": - return subscripts(object_(), allow_calls); + return subscripts(object_or_object_destructuring_(), allow_calls); } unexpected(); } @@ -1334,68 +1321,101 @@ function parse($TEXT, options) { }); }); - var object_ = embed_tokens(function() { + var object_or_object_destructuring_ = embed_tokens(function() { var start = S.token; expect("{"); - var first = true, a = []; - while (!is("punc", "}")) { - if (first) first = false; else expect(","); - if (!options.strict && is("punc", "}")) - // allow trailing comma - break; - var start = S.token; - var type = start.type; - var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; + function try_an_object() { + var first = true, a = []; + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!options.strict && is("punc", "}")) + // allow trailing comma + break; + var start = S.token; + var type = start.type; + var name = as_property_name(); + if (type == "name" && !is("punc", ":")) { + if (name == "get") { + a.push(new AST_ObjectGetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; + } + if (name == "set") { + a.push(new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; + } } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - } - - if (!is("punc", ":")) { - // It's one of those object destructurings, the value is its own name - if (!S.in_parameters) { - croak("Invalid syntax", S.token.line, S.token.col); - } - a.push(new AST_ObjectSymbol({ - start: start, - end: start, - symbol: new AST_SymbolRef({ + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + if (!S.in_parameters) { + croak("Invalid syntax", S.token.line, S.token.col); + } + a.push(new AST_ObjectSymbol({ start: start, end: start, - name: name - }) - })); - } else { - if (S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + if (S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); } - expect(":"); - a.push(new AST_ObjectKeyVal({ - start : start, - key : name, - value : expression(false), - end : prev() - })); } + next(); + return new AST_Object({ properties: a }) } - next(); - return new AST_Object({ properties: a }); + + var obj = try_an_object(); + if (obj instanceof AST_Object) { return obj; } + + if (!S.in_parameters) { + croak("Cannot destructure", S.token.line, S.token.col); + } + + var firstName = obj; + + var namesInDestructuring = []; + + namesInDestructuring.push( new AST_SymbolRef({ + start : prev(), + end : prev(), + name : firstName + })); + + while (!is("punc", "}")) { + expect(","); + namesInDestructuring.push(as_symbol(AST_SymbolRef)) + } + + expect('}'); + + return new AST_Destructuring({ + start : start, + end : S.token, + names : namesInDestructuring, + is_array : false + }) }); function as_property_name() { @@ -1553,16 +1573,13 @@ function parse($TEXT, options) { return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); }; + // In ES6, AssignmentExpression can also be an ArrowFunction var maybe_assign = function(no_in) { var start = S.token; - if (start.value == "(" && peek().value == ")") { - next(); // ( - next(); // ) - return arrow_function([]); - } + var left = maybe_conditional(no_in); + var val = S.token.value; - var left = maybe_conditional(no_in), val = S.token.value; if (is("operator") && ASSIGNMENT(val)) { if (is_assignable(left)) { next(); @@ -1576,15 +1593,20 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } - if (is("arrow")) { - return arrow_function(left) - } return left; }; var expression = function(commas, no_in) { var start = S.token; var expr = maybe_assign(no_in); + if (expr instanceof AST_SymbolRef && is("arrow", "=>")) { + expr = new AST_ArrowParametersOrSeq({ + start: expr.start, + end: expr.end, + expressions: [expr] + }); + return arrow_function(expr); + } if (commas && is("punc", ",")) { next(); return new AST_Seq({ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 3c07c49e..02d2b299 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -35,3 +35,65 @@ regression_arrow_functions_and_hoist: { } expect_exact: "a=>b;" } + +destructuring_arguments: { + input: { + (function ( a ) { }); + (function ( [ a ] ) { }); + (function ( [ a, b ] ) { }); + (function ( [ [ a ] ] ) { }); + (function ( [ [ a, b ] ] ) { }); + (function ( [ a, [ b ] ] ) { }); + (function ( [ [ b ], a ] ) { }); + + (function ( { a } ) { }); + (function ( { a, b } ) { }); + + (function ( [ { a } ] ) { }); + (function ( [ { a, b } ] ) { }); + (function ( [ a, { b } ] ) { }); + (function ( [ { b }, a ] ) { }); + + ( [ a ] ) => { }; + ( [ a, b ] ) => { }; + + ( { a } ) => { }; + ( { a, b, c, d, e } ) => { }; + + ( [ a ] ) => b; + ( [ a, b ] ) => c; + + ( { a } ) => b; + ( { a, b } ) => c; + } + expect: { + (function(a){}); + (function([a]){}); + (function([a,b]){}); + (function([[a]]){}); + (function([[a,b]]){}); + (function([a,[b]]){}); + (function([[b],a]){}); + + (function({a}){}); + (function({a,b}){}); + + (function([{a}]){}); + (function([{a,b}]){}); + (function([a,{b}]){}); + (function([{b},a]){}); + + ([a])=>{}; + ([a,b])=>{}; + + ({a})=>{}; + ({a,b,c,d,e})=>{}; + + ([a])=>b; + ([a,b])=>c; + + ({a})=>b; + ({a,b})=>c; + } +} + diff --git a/test/parser.js b/test/parser.js index b512f1ca..e661907a 100644 --- a/test/parser.js +++ b/test/parser.js @@ -21,6 +21,12 @@ module.exports = function () { ok.equal(destr_fun1.argnames.length, 1); ok.equal(destr_fun2.argnames.length, 1); + var destr_fun1 = UglifyJS.parse('({a, b}) => null').body[0].body; + var destr_fun2 = UglifyJS.parse('([a, [b]]) => null').body[0].body; + + ok.equal(destr_fun1.argnames.length, 1); + ok.equal(destr_fun2.argnames.length, 1); + var destruct1 = destr_fun1.argnames[0]; var destruct2 = destr_fun2.argnames[0]; From 7112c49c92457a655a2307c6f8806b02a4abbafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Wed, 5 Aug 2015 11:49:31 +0100 Subject: [PATCH 047/105] Uglifyjs already supports super as an implicit global! Just adding a test to indicate that. --- test/compress/super.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/compress/super.js diff --git a/test/compress/super.js b/test/compress/super.js new file mode 100644 index 00000000..d297a84e --- /dev/null +++ b/test/compress/super.js @@ -0,0 +1,9 @@ + +super_can_be_parsed: { + input: { + super(1,2); + super.meth(); + } + expect_exact: "super(1,2);super.meth();" +} + From 7f825c824472211ce16bfdbab7309b47a2eda08d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 13 Apr 2015 01:26:26 +0100 Subject: [PATCH 048/105] expand parameters Conflicts: test/compress/harmony.js --- lib/ast.js | 26 +++++++++++++++++++-- lib/compress.js | 3 +++ lib/output.js | 5 +++++ lib/parse.js | 45 ++++++++++++++++++++++++++++++++----- test/compress/expansions.js | 17 ++++++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 test/compress/expansions.js diff --git a/lib/ast.js b/lib/ast.js index 3baa63cf..5d2f9347 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -363,10 +363,27 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); +// TODO besides parameters and function calls, expansions can go in +// arrays, array destructuring parameters, and array destructuring +// assignment. But I'm not adding this right now because I'm trying +// to do the most minimal and independent changesets. +var AST_Expansion = DEFNODE("AST_Expansion", "symbol", { + $documentation: "An expandible argument, such as ...rest", + $propdoc: { + symbol: "AST_SymbolFunarg the name of the argument as a SymbolFunarg" + }, + _walk: function(visitor) { + var self = this; + return visitor._visit(this, function(){ + self.symbol.walk(visitor); + }); + } +}); + var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { $documentation: "A set of arrow function parameters or a sequence expression. This is used because when the parser sees a \"(\" it could be the start of a seq, or the start of a parameter list of an arrow function.", $propdoc: { - expressions: "[AST_Expression|AST_Destructuring*] array of expressions or argument names or destructurings." + expressions: "[AST_Expression|AST_Destructuring|AST_Expansion*] array of expressions or argument names or destructurings." }, as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring @@ -398,6 +415,8 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end }); + } else if (ex instanceof AST_Expansion) { + return ex; } else if (ex instanceof AST_Array) { if (ex.elements.length === 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -421,7 +440,7 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { $documentation: "Base class for functions", $propdoc: { name: "[AST_SymbolDeclaration?] the name of this function", - argnames: "[AST_SymbolFunarg|AST_Destructuring*] array of function arguments or destructurings", + argnames: "[AST_SymbolFunarg|AST_Destructuring|AST_Expansion*] array of function arguments, destructurings, or expanding arguments", uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" }, args_as_names: function () { @@ -431,6 +450,9 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { if (parm instanceof AST_SymbolFunarg) { out.push(parm); } + if (parm instanceof AST_Expansion) { + out.push(parm.symbol); + } })); return out; }, diff --git a/lib/compress.js b/lib/compress.js index aa54db1b..983e31df 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1318,6 +1318,9 @@ merge(Compressor.prototype, { break; } else { var sym = a[i]; + if (sym instanceof AST_Expansion) { + sym = sym.symbol; + } if (sym.unreferenced()) { a.pop(); compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { diff --git a/lib/output.js b/lib/output.js index d2d6cedb..4ebaf055 100644 --- a/lib/output.js +++ b/lib/output.js @@ -630,6 +630,11 @@ function OutputStream(options) { output.semicolon(); }); + DEFPRINT(AST_Expansion, function (self, output) { + output.print('...'); + self.symbol.print(output); + }); + DEFPRINT(AST_Destructuring, function (self, output) { output.print(self.is_array ? "[" : "{"); var first = true; diff --git a/lib/parse.js b/lib/parse.js index 3e6c826c..054cc1a6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -530,9 +530,16 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { function handle_dot() { next(); - return is_digit(peek().charCodeAt(0)) - ? read_num(".") - : token("punc", "."); + if (is_digit(peek().charCodeAt(0))) { + return read_num("."); + } + if (peek() === ".") { + next(); // Consume second dot + next(); // Consume third dot + return token("expand", "..."); + } + + return token("punc", "."); }; function read_word() { @@ -1046,7 +1053,16 @@ function parse($TEXT, options) { S.in_parameters = true; while (!is("punc", ")")) { if (first) first = false; else expect(","); - a.push(expression(false)); + if (is("expand", "...")) { + next(); + a.push(new AST_Expansion({ + start: prev(), + symbol: as_symbol(AST_SymbolFunarg), + end: S.token, + })); + } else { + a.push(expression(false)); + } } S.in_parameters = false; var end = S.token @@ -1494,13 +1510,32 @@ function parse($TEXT, options) { return subscripts(new AST_Call({ start : start, expression : expr, - args : expr_list(")"), + args : call_args(), end : prev() }), true); } return expr; }; + var call_args = embed_tokens(function call_args() { + var first = true; + var args = []; + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + if (is("expand", "...")) { + next(); + args.push(new AST_Expansion({ + start: prev(), + symbol: as_symbol(AST_SymbolFunarg) + })); + } else { + args.push(expression(false)); + } + } + next(); + return args; + }); + var maybe_unary = function(allow_calls) { var start = S.token; if (is("operator") && UNARY_PREFIX(start.value)) { diff --git a/test/compress/expansions.js b/test/compress/expansions.js new file mode 100644 index 00000000..a6537547 --- /dev/null +++ b/test/compress/expansions.js @@ -0,0 +1,17 @@ + +expand_arguments: { + input: { + func(a, ...rest); + func(...all); + } + expect_exact: "func(a,...rest);func(...all);" +} + +expand_parameters: { + input: { + (function (a, ...b){}); + (function (...args){}); + } + expect_exact: "(function(a,...b){});(function(...args){});" +} + From 0a4270b35812ca9fa56ddb44c827ad33cbd17bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 7 Aug 2015 02:44:53 +0100 Subject: [PATCH 049/105] Super! --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 5 ++++- lib/scope.js | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 5d2f9347..b64a6d8b 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -967,6 +967,10 @@ var AST_This = DEFNODE("This", null, { $documentation: "The `this` symbol", }, AST_Symbol); +var AST_Super = DEFNODE("Super", null, { + $documentation: "The `super` symbol", +}, AST_Symbol); + var AST_Constant = DEFNODE("Constant", null, { $documentation: "Base class for all constants", getValue: function() { diff --git a/lib/output.js b/lib/output.js index 4ebaf055..f87bdd9e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1223,6 +1223,9 @@ function OutputStream(options) { DEFPRINT(AST_This, function(self, output){ output.print("this"); }); + DEFPRINT(AST_Super, function(self, output){ + output.print("super"); + }); DEFPRINT(AST_Constant, function(self, output){ output.print(self.getValue()); }); diff --git a/lib/parse.js b/lib/parse.js index 054cc1a6..bfda6eb0 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1466,7 +1466,9 @@ function parse($TEXT, options) { function _make_symbol(type) { var name = S.token.value; - return new (name == "this" ? AST_This : type)({ + return new (name == "this" ? AST_This : + name == "super" ? AST_Super : + type)({ name : String(name), start : S.token, end : S.token @@ -1605,6 +1607,7 @@ function parse($TEXT, options) { function is_assignable(expr) { if (!options.strict) return true; if (expr instanceof AST_This) return false; + if (expr instanceof AST_Super) return false; return (expr instanceof AST_PropAccess || expr instanceof AST_Symbol); }; diff --git a/lib/scope.js b/lib/scope.js index dee748f9..8b60ce8f 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -518,6 +518,8 @@ AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options){ base54.consider("new"); else if (node instanceof AST_This) base54.consider("this"); + else if (node instanceof AST_Super) + base54.consider("super"); else if (node instanceof AST_Try) base54.consider("try"); else if (node instanceof AST_Catch) From a4d6ed076afff7c093ad14590545ef37955d421e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 03:24:54 +0100 Subject: [PATCH 050/105] Parse binary number literals --- lib/parse.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index bfda6eb0..556b1a64 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -59,6 +59,8 @@ var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; var RE_OCT_NUMBER = /^0[0-7]+$/; +var RE_BIN_NUMBER = /^0b[01]+$/; +var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; var OPERATORS = makePredicate([ "in", @@ -181,9 +183,10 @@ function parse_js_number(num) { return parseInt(num.substr(2), 16); } else if (RE_OCT_NUMBER.test(num)) { return parseInt(num.substr(1), 8); - } else { - var val = parseFloat(num); - if (val == num) return val; + } else if (RE_BIN_NUMBER.test(num)) { + return parseInt(num.substr(2), 2); + } else if (RE_DEC_NUMBER.test(num)) { + return parseFloat(num); } }; From 6f93a8d4a1633851ab6e756e2f54e1d8c8cfa423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 22:05:42 +0100 Subject: [PATCH 051/105] Fix evaluating the typeof an arrow function. Using evaluate on used to cause a crash. --- lib/compress.js | 6 +++++- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/compress.js b/lib/compress.js index 983e31df..35ea6c77 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -948,6 +948,9 @@ merge(Compressor.prototype, { // places too. :-( Wish JS had multiple inheritance. throw def; }); + def(AST_Arrow, function() { + throw def; + }); function ev(node, compressor) { if (!compressor) throw new Error("Compressor must be passed"); @@ -966,7 +969,8 @@ merge(Compressor.prototype, { case "typeof": // Function would be evaluated to an array and so typeof would // incorrectly return 'object'. Hence making is a special case. - if (e instanceof AST_Function) return typeof function(){}; + if (e instanceof AST_Function || + e instanceof AST_Arrow) return typeof function(){}; e = ev(e, compressor); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 02d2b299..812ad9d9 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,16 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +typeof_arrow_functions: { + options = { + evaluate: true + } + input: { + typeof (x) => null; + } + expect_exact: "\"function\";" +} + destructuring_arguments: { input: { (function ( a ) { }); From ac1d3f0e8891b250e756d2c8266d632ef6b7f2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 22:44:16 +0100 Subject: [PATCH 052/105] remove Symbol's argument when we're unsafe and it's undeclared --- lib/compress.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/compress.js b/lib/compress.js index 35ea6c77..b6481198 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -2062,6 +2062,11 @@ merge(Compressor.prototype, { } } break; + case "Symbol": + // Symbol's argument is only used for debugging. + self.args = []; + return self; + break; } } else if (exp instanceof AST_Dot && exp.property == "toString" && self.args.length == 0) { From ebd0682376f1d4c4fb898488c45be8168882a58d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 17 Aug 2015 11:50:56 +0100 Subject: [PATCH 053/105] Add new-style octal literals and make the B and the O case insensitive. --- lib/parse.js | 5 ++++- test/compress/harmony.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 556b1a64..2213b919 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -59,7 +59,8 @@ var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; var RE_OCT_NUMBER = /^0[0-7]+$/; -var RE_BIN_NUMBER = /^0b[01]+$/; +var RE_ES6_OCT_NUMBER = /^0o[0-7]+$/i; +var RE_BIN_NUMBER = /^0b[01]+$/i; var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; var OPERATORS = makePredicate([ @@ -183,6 +184,8 @@ function parse_js_number(num) { return parseInt(num.substr(2), 16); } else if (RE_OCT_NUMBER.test(num)) { return parseInt(num.substr(1), 8); + } else if (RE_ES6_OCT_NUMBER.test(num)) { + return parseInt(num.substr(2), 8); } else if (RE_BIN_NUMBER.test(num)) { return parseInt(num.substr(2), 2); } else if (RE_DEC_NUMBER.test(num)) { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 812ad9d9..d7865675 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -107,3 +107,18 @@ destructuring_arguments: { } } +binary_literals: { + input: { + 0b1001; + 0B1001; + 0o11; + 0O11; + } + + expect: { + 9; + 9; + 9; + 9; + } +} From 046e30366970dbcb2b9b9386f55186c12a6cf49c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 17 Aug 2015 16:23:43 +0100 Subject: [PATCH 054/105] s/binary/number/g --- test/compress/harmony.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index d7865675..2fcfd70f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -107,7 +107,7 @@ destructuring_arguments: { } } -binary_literals: { +number_literals: { input: { 0b1001; 0B1001; From 16e928348bef2bf8dbecc9cdb9e51669cacaf083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 13 Aug 2015 02:56:57 +0100 Subject: [PATCH 055/105] prepare AST_Destructuring for the Ents --- lib/transform.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/transform.js b/lib/transform.js index 0087a0a8..34663351 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -163,6 +163,10 @@ TreeTransformer.prototype = new TreeWalker; if (self.value) self.value = self.value.transform(tw); }); + _(AST_Destructuring, function(self, tw) { + self.names = do_list(self.names, tw); + }); + _(AST_Lambda, function(self, tw){ if (self.name) self.name = self.name.transform(tw); self.argnames = do_list(self.argnames, tw); From 5efbe1604e5ffeb72c5bd89938394946fad8afeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 13 Aug 2015 02:58:08 +0100 Subject: [PATCH 056/105] A little refactoring. Add a new function to get all symbols in a destructuring. --- lib/ast.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index b64a6d8b..8c3a26fc 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -445,15 +445,13 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { }, args_as_names: function () { var out = []; - this.walk(new TreeWalker(function (parm) { - var that = this; - if (parm instanceof AST_SymbolFunarg) { - out.push(parm); + for (var i = 0; i < this.argnames.length; i++) { + if (this.argnames[i] instanceof AST_Destructuring) { + out = out.concat(this.argnames[i].all_symbols()); + } else { + out.push(this.argnames[i]); } - if (parm instanceof AST_Expansion) { - out.push(parm.symbol); - } - })); + } return out; }, _walk: function(visitor) { @@ -492,6 +490,18 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { name._walk(visitor); }); }); + }, + all_symbols: function() { + var out = []; + this.walk(new TreeWalker(function (node) { + if (node instanceof AST_Symbol) { + out.push(node); + } + if (node instanceof AST_Expansion) { + out.push(node.symbol); + } + })); + return out; } }); From 3b189f86ffd41b0efadd01c8df154e11a74a74f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 00:20:21 +0100 Subject: [PATCH 057/105] Parse and compress destructuring VarDefs --- lib/ast.js | 5 ++++- lib/compress.js | 15 ++++++++++++++- lib/parse.js | 33 +++++++++++++++++++++++++++------ test/compress/destructuring.js | 19 +++++++++++++++++++ test/compress/drop-unused.js | 15 +++++++++++++++ test/compress/hoist.js | 18 ++++++++++++++++++ test/parser.js | 8 ++++++++ 7 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 test/compress/destructuring.js diff --git a/lib/ast.js b/lib/ast.js index 8c3a26fc..45bab4d5 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -665,9 +665,12 @@ var AST_Const = DEFNODE("Const", null, { var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { - name: "[AST_SymbolVar|AST_SymbolConst] name of the variable", + name: "[AST_SymbolVar|AST_SymbolConst|AST_Destructuring] name of the variable", value: "[AST_Node?] initializer, or null of there's no initializer" }, + is_destructuring: function() { + return this.name instanceof AST_Destructuring; + }, _walk: function(visitor) { return visitor._visit(this, function(){ this.name._walk(visitor); diff --git a/lib/compress.js b/lib/compress.js index b6481198..d7f9feda 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1267,6 +1267,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && scope === self) { node.definitions.forEach(function(def){ + if (def.is_destructuring()) return; /* Destructurings are type assertions! */ if (def.value) { initializations.add(def.name.name, def.value); if (def.value.has_side_effects(compressor)) { @@ -1353,6 +1354,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { var def = node.definitions.filter(function(def){ + if (def.is_destructuring()) return true; if (member(def.name.definition(), in_use)) return true; var w = { name : def.name.name, @@ -1475,6 +1477,7 @@ merge(Compressor.prototype, { } if (node instanceof AST_Var && hoist_vars) { node.definitions.forEach(function(def){ + if (def.is_destructuring()) { return; } vars.set(def.name.name, def); ++vars_found; }); @@ -1929,13 +1932,23 @@ merge(Compressor.prototype, { AST_Definitions.DEFMETHOD("to_assignments", function(){ var assignments = this.definitions.reduce(function(a, def){ - if (def.value) { + if (def.value && !def.is_destructuring()) { var name = make_node(AST_SymbolRef, def.name, def.name); a.push(make_node(AST_Assign, def, { operator : "=", left : name, right : def.value })); + } else if (def.value) { + // Because it's a destructuring, do not turn into an assignment. + var varDef = make_node(AST_VarDef, def, { + name: def.name, + value: def.value + }); + var var_ = make_node(AST_Var, def, { + definitions: [ varDef ] + }); + a.push(var_); } return a; }, []); diff --git a/lib/parse.js b/lib/parse.js index 2213b919..7956f175 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1192,13 +1192,24 @@ function parse($TEXT, options) { function vardefs(no_in, in_const) { var a = []; + var def; for (;;) { - a.push(new AST_VarDef({ - start : S.token, - name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), - value : is("operator", "=") ? (next(), expression(false, no_in)) : null, - end : prev() - })); + if (is("punc", "{") || is("punc", "[")) { + def = new AST_VarDef({ + start: S.token, + name: destructuring_(), + value: (expect_token("operator", "="), expression(false, no_in)), + end: prev() + }); + } else { + def = new AST_VarDef({ + start : S.token, + name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), + value : is("operator", "=") ? (next(), expression(false, no_in)) : null, + end : prev() + }) + } + a.push(def); if (!is("punc", ",")) break; next(); @@ -1206,6 +1217,16 @@ function parse($TEXT, options) { return a; }; + var destructuring_ = embed_tokens(function () { + var is_array = is("punc", "["); + var closing = is_array ? ']' : '}'; + next() + return new AST_Destructuring({ + names: expr_list(closing), + is_array: is_array + }) + }); + var var_ = function(no_in) { return new AST_Var({ start : prev(), diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js new file mode 100644 index 00000000..58a77cd4 --- /dev/null +++ b/test/compress/destructuring.js @@ -0,0 +1,19 @@ + +destructuring_arrays: { + input: { + var [aa, bb] = cc; + } + expect: { + var[aa,bb]=cc; + } +} + +destructuring_objects: { + input: { + var {aa, bb} = {aa:1, bb:2}; + } + expect: { + var{aa,bb}={aa:1,bb:2}; + } +} + diff --git a/test/compress/drop-unused.js b/test/compress/drop-unused.js index 035a428e..eebb81c6 100644 --- a/test/compress/drop-unused.js +++ b/test/compress/drop-unused.js @@ -164,6 +164,21 @@ used_var_in_catch: { } } +unused_keep_harmony_destructuring: { + options = { unused: true }; + input: { + function foo() { + var {x, y} = foo; + var a = foo; + } + } + expect: { + function foo() { + var {x, y} = foo; + } + } +} + keep_fnames: { options = { unused: true, keep_fnames: true, unsafe: true }; input: { diff --git a/test/compress/hoist.js b/test/compress/hoist.js index 2b359fc5..68f5a8c3 100644 --- a/test/compress/hoist.js +++ b/test/compress/hoist.js @@ -65,3 +65,21 @@ hoist_no_destructurings: { } } } + +dont_hoist_var_destructurings: { + options = { + hoist_vars: true, + hoist_funs: true + } + input: { + function x() { + // If foo is null or undefined, this should be an exception + var {x,y} = foo; + } + } + expect: { + function x() { + var {x,y} = foo; + } + } +} diff --git a/test/parser.js b/test/parser.js index e661907a..85c2e23e 100644 --- a/test/parser.js +++ b/test/parser.js @@ -94,6 +94,14 @@ module.exports = function () { UglifyJS.parse('(function [a] { })'); }); + // Destructuring variable declaration + + var decls = UglifyJS.parse('var {a,b} = foo, { c, d } = bar'); + + ok.equal(decls.body[0].TYPE, 'Var'); + ok.equal(decls.body[0].definitions.length, 2); + ok.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); + ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement // destructuring (right now it's only destructuring *arguments*), From a3a4ae5ef8502c4c28b4061b6ad979a5dc19720c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 01:16:54 +0100 Subject: [PATCH 058/105] Add holes in destructuring defs, also make them nestable --- lib/parse.js | 30 +++++++++++++++++++++++++----- test/compress/destructuring.js | 6 ++++++ test/parser.js | 11 +++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 7956f175..5ece68fd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -765,7 +765,7 @@ function parse($TEXT, options) { function embed_tokens(parser) { return function() { var start = S.token; - var expr = parser(); + var expr = parser.apply(null, arguments); var end = prev(); expr.start = start; expr.end = end; @@ -1194,17 +1194,18 @@ function parse($TEXT, options) { var a = []; var def; for (;;) { + var sym_type = in_const ? AST_SymbolConst : AST_SymbolVar; if (is("punc", "{") || is("punc", "[")) { def = new AST_VarDef({ start: S.token, - name: destructuring_(), + name: destructuring_(sym_type), value: (expect_token("operator", "="), expression(false, no_in)), end: prev() }); } else { def = new AST_VarDef({ start : S.token, - name : as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), + name : as_symbol(sym_type), value : is("operator", "=") ? (next(), expression(false, no_in)) : null, end : prev() }) @@ -1217,12 +1218,31 @@ function parse($TEXT, options) { return a; }; - var destructuring_ = embed_tokens(function () { + var destructuring_ = embed_tokens(function (sym_type) { var is_array = is("punc", "["); var closing = is_array ? ']' : '}'; + var sym_type = sym_type || AST_SymbolRef; + + next(); + + var first = true, children = []; + while (!is("punc", closing)) { + if (first) first = false; else expect(","); + if (is("punc", closing)) break; + if (is("punc", ",")) { + children.push(new AST_Hole({ start: S.token, end: S.token })); + } else if (is("punc", "[") || is("punc", "{")) { + children.push(destructuring_(sym_type)); + } else if (is("name")) { + children.push(_make_symbol(sym_type)); + next(); + } else { + children.push(expression()); + } + } next() return new AST_Destructuring({ - names: expr_list(closing), + names: children, is_array: is_array }) }); diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 58a77cd4..b667c9ea 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -17,3 +17,9 @@ destructuring_objects: { } } +nested_destructuring_objects: { + input: { + var [{a},b] = c; + } + expect_exact: 'var[{a},b]=c;'; +} diff --git a/test/parser.js b/test/parser.js index 85c2e23e..125f76c2 100644 --- a/test/parser.js +++ b/test/parser.js @@ -101,6 +101,17 @@ module.exports = function () { ok.equal(decls.body[0].TYPE, 'Var'); ok.equal(decls.body[0].definitions.length, 2); ok.equal(decls.body[0].definitions[0].name.TYPE, 'Destructuring'); + ok.equal(decls.body[0].definitions[0].value.TYPE, 'SymbolRef'); + + var nested_def = UglifyJS.parse('var [{x}] = foo').body[0].definitions[0]; + + ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar') + ok.equal(nested_def.name.names[0].names[0].name, 'x') + + var holey_def = UglifyJS.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; + + ok.equal(holey_def.name.names[0].TYPE, 'Hole') + ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst') ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement From d757202b74969495d662d58b4d7da9c4c12e8a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:00:31 +0100 Subject: [PATCH 059/105] Do not mangle a name if it is in a destructuring vardef. --- lib/scope.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 8b60ce8f..2388fb52 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -98,7 +98,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var defun = null; var last_var_had_const_pragma = false; var nesting = 0; - var object_destructuring_arg = false; + var in_destructuring = null; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -110,9 +110,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ return true; } if (node instanceof AST_Destructuring && node.is_array === false) { - object_destructuring_arg = true; // These don't nest + in_destructuring = node; // These don't nest descend(); - object_destructuring_arg = false; + in_destructuring = null; return true; } if (node instanceof AST_Scope) { @@ -151,7 +151,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.references = []; } if (node instanceof AST_SymbolFunarg) { - node.object_destructuring_arg = object_destructuring_arg; + node.object_destructuring_arg = !!in_destructuring; defun.def_variable(node); } if (node instanceof AST_SymbolLambda) { @@ -171,7 +171,8 @@ 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 || last_var_had_const_pragma; + def.constant = node instanceof AST_SymbolConst; + def.destructuring = in_destructuring; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -458,7 +459,10 @@ AST_Toplevel.DEFMETHOD("mangle_names", function(options){ } }); this.walk(tw); - to_mangle.forEach(function(def){ def.mangle(options) }); + to_mangle.forEach(function(def){ + if (def.destructuring && !def.destructuring.is_array) return; + def.mangle(options); + }); if (options.cache) { options.cache.cname = this.cname; From a170ad9d0dd3361d81e1d72760f63dbcc9b2b160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:11:38 +0100 Subject: [PATCH 060/105] Destructuring vardef in for..of and for..in --- lib/parse.js | 2 +- test/compress/destructuring.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 5ece68fd..e892b418 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1199,7 +1199,7 @@ function parse($TEXT, options) { def = new AST_VarDef({ start: S.token, name: destructuring_(sym_type), - value: (expect_token("operator", "="), expression(false, no_in)), + value: is("operator", "=") ? (expect_token("operator", "="), expression(false, no_in)) : null, end: prev() }); } else { diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index b667c9ea..30fbbee8 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -23,3 +23,12 @@ nested_destructuring_objects: { } expect_exact: 'var[{a},b]=c;'; } + +destructuring_vardef_in_loops: { + input: { + for (var [x,y] in pairs); + for (var [a] = 0;;); + for (var {c} of cees); + } + expect_exact: "for(var[x,y]in pairs);for(var[a]=0;;);for(var{c}of cees);" +} From 048eac6b2983a918184a2afce160a31ed5f65d87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 14 Aug 2015 02:19:53 +0100 Subject: [PATCH 061/105] Tolerate expansions in vardefs, too! --- lib/ast.js | 10 +++------- lib/parse.js | 9 +++++++++ test/parser.js | 14 ++++++++++---- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 45bab4d5..52582fdd 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -363,14 +363,10 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", { } }, AST_Scope); -// TODO besides parameters and function calls, expansions can go in -// arrays, array destructuring parameters, and array destructuring -// assignment. But I'm not adding this right now because I'm trying -// to do the most minimal and independent changesets. -var AST_Expansion = DEFNODE("AST_Expansion", "symbol", { - $documentation: "An expandible argument, such as ...rest", +var AST_Expansion = DEFNODE("Expansion", "symbol", { + $documentation: "An expandible argument, such as ...rest, a splat, such as [1,2,...all], or an expansion in a variable declaration, such as var [first, ...rest] = list", $propdoc: { - symbol: "AST_SymbolFunarg the name of the argument as a SymbolFunarg" + symbol: "AST_Symbol the thing to be expanded" }, _walk: function(visitor) { var self = this; diff --git a/lib/parse.js b/lib/parse.js index e892b418..4fc2acba 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1233,6 +1233,15 @@ function parse($TEXT, options) { children.push(new AST_Hole({ start: S.token, end: S.token })); } else if (is("punc", "[") || is("punc", "{")) { children.push(destructuring_(sym_type)); + } else if (is("expand", "...")) { + next(); + var symbol = _make_symbol(sym_type); + children.push(new AST_Expansion({ + start: prev(), + symbol: symbol, + end: S.token + })); + next(); } else if (is("name")) { children.push(_make_symbol(sym_type)); next(); diff --git a/test/parser.js b/test/parser.js index 125f76c2..66676b9d 100644 --- a/test/parser.js +++ b/test/parser.js @@ -105,13 +105,19 @@ module.exports = function () { var nested_def = UglifyJS.parse('var [{x}] = foo').body[0].definitions[0]; - ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar') - ok.equal(nested_def.name.names[0].names[0].name, 'x') + ok.equal(nested_def.name.names[0].names[0].TYPE, 'SymbolVar'); + ok.equal(nested_def.name.names[0].names[0].name, 'x'); var holey_def = UglifyJS.parse('const [,,third] = [1,2,3]').body[0].definitions[0]; - ok.equal(holey_def.name.names[0].TYPE, 'Hole') - ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst') + ok.equal(holey_def.name.names[0].TYPE, 'Hole'); + ok.equal(holey_def.name.names[2].TYPE, 'SymbolConst'); + + var expanding_def = UglifyJS.parse('var [first, ...rest] = [1,2,3]').body[0].definitions[0]; + + ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); + ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); + ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); ok.throws(function () { // Note: this *is* a valid destructuring, but before we implement From cfb1b5d848cf4d8d034dbf3219cb70fca85a8857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Thu, 15 Jan 2015 03:03:38 +0000 Subject: [PATCH 062/105] Starting destructuring expressions --- lib/output.js | 11 +++++++++++ lib/parse.js | 3 --- test/compress/destructuring.js | 9 +++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/output.js b/lib/output.js index f87bdd9e..95865b53 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1210,6 +1210,17 @@ function OutputStream(options) { var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); }); + DEFPRINT(AST_ObjectSymbol, function(self, output){ + var def = self.symbol.definition(); + if (def && def.mangled_name) { + output.print(self.symbol.name); + output.print(':'); + output.space(); + output.print(def.mangled_name); + } else { + output.print(self.symbol.name); + } + }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index 4fc2acba..92c2f261 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1429,9 +1429,6 @@ function parse($TEXT, options) { if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name - if (!S.in_parameters) { - croak("Invalid syntax", S.token.line, S.token.col); - } a.push(new AST_ObjectSymbol({ start: start, end: start, diff --git a/test/compress/destructuring.js b/test/compress/destructuring.js index 30fbbee8..6ea54ac1 100644 --- a/test/compress/destructuring.js +++ b/test/compress/destructuring.js @@ -32,3 +32,12 @@ destructuring_vardef_in_loops: { } expect_exact: "for(var[x,y]in pairs);for(var[a]=0;;);for(var{c}of cees);" } +destructuring_expressions: { + input: { + ({a, b}); + [{a}]; + f({x}); + } + expect_exact: "({a,b});[{a}];f({x});" +} + From 2d74aca46a05de65c3039b762f2f8172857324d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 8 Aug 2015 16:01:16 +0100 Subject: [PATCH 063/105] play nice with propmangle --- lib/output.js | 5 +++-- lib/propmangle.js | 8 ++++++++ lib/transform.js | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/output.js b/lib/output.js index 95865b53..dc550a7c 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1211,14 +1211,15 @@ function OutputStream(options) { output.print_name(def ? def.mangled_name || def.name : self.name); }); DEFPRINT(AST_ObjectSymbol, function(self, output){ + var name = self.mangled_key || self.symbol.name; var def = self.symbol.definition(); if (def && def.mangled_name) { - output.print(self.symbol.name); + output.print(name); output.print(':'); output.space(); output.print(def.mangled_name); } else { - output.print(self.symbol.name); + output.print(name); } }); DEFPRINT(AST_Undefined, function(self, output){ diff --git a/lib/propmangle.js b/lib/propmangle.js index 840bda91..ff782b57 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -90,6 +90,9 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { add(node.key); } + else if (node instanceof AST_ObjectSymbol) { + add(node.symbol.name); + } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above add(node.key.name); @@ -111,6 +114,11 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { node.key = mangle(node.key); } + else if (node instanceof AST_ObjectSymbol) { + if (should_mangle(node.symbol.name)) { + node.mangled_key = mangle(node.symbol.name) + } + } else if (node instanceof AST_ObjectProperty) { // setter or getter node.key.name = mangle(node.key.name); diff --git a/lib/transform.js b/lib/transform.js index 34663351..5cb5c104 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -219,6 +219,10 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); + _(AST_ObjectSymbol, function(self, tw){ + self.symbol = self.symbol.transform(tw); + }); + _(AST_ObjectProperty, function(self, tw){ self.value = self.value.transform(tw); }); From a54b64d660ca5d3300f43e7fe80a03a757ba7129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 10 Aug 2015 21:13:30 +0100 Subject: [PATCH 064/105] What about --mangle-props being on and --mangle being off? --- lib/output.js | 5 +++++ test/parser.js | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/output.js b/lib/output.js index dc550a7c..8dfe10f8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1218,6 +1218,11 @@ function OutputStream(options) { output.print(':'); output.space(); output.print(def.mangled_name); + } else if (!(def && def.mangled_name) && self.mangled_key) { + output.print(name); + output.print(':'); + output.space(); + output.print(def.name); } else { output.print(name); } diff --git a/test/parser.js b/test/parser.js index 66676b9d..a84c2df9 100644 --- a/test/parser.js +++ b/test/parser.js @@ -118,13 +118,6 @@ module.exports = function () { ok.equal(expanding_def.name.names[0].TYPE, 'SymbolVar'); ok.equal(expanding_def.name.names[1].TYPE, 'Expansion'); ok.equal(expanding_def.name.names[1].symbol.TYPE, 'SymbolVar'); - - ok.throws(function () { - // Note: this *is* a valid destructuring, but before we implement - // destructuring (right now it's only destructuring *arguments*), - // this won't do. - UglifyJS.parse('[{a}]'); - }); } // Run standalone From e0b78bfa5f87836c0247c00d8c34929094e2f381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 22:32:57 +0100 Subject: [PATCH 065/105] Parse and output ES6 template strings. Yikes! --- lib/ast.js | 16 ++++++++++++++++ lib/output.js | 14 ++++++++++++++ lib/parse.js | 40 +++++++++++++++++++++++++++++++++++++++- test/compress/harmony.js | 10 ++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 52582fdd..6142bae2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -501,6 +501,22 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { } }); +var AST_TemplateString = DEFNODE("TemplateString", "segments", { + $documentation: "A template string literal", + $propdoc: { + segments: "[string|AST_Expression]* One or more segments. They can be the parts that are evaluated, or the raw string parts." + }, + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.segments.forEach(function(seg, i){ + if (i % 2 !== 0) { + seg._walk(visitor); + } + }); + }); + } +}); + /* -----[ JUMPS ]----- */ var AST_Jump = DEFNODE("Jump", null, { diff --git a/lib/output.js b/lib/output.js index 8dfe10f8..a9486bc2 100644 --- a/lib/output.js +++ b/lib/output.js @@ -799,6 +799,20 @@ function OutputStream(options) { self._do_print(output); }); + DEFPRINT(AST_TemplateString, function(self, output) { + output.print("`"); + for (var i = 0; i < self.segments.length; i++) { + if (typeof self.segments[i] !== "string") { + output.print("${"); + self.segments[i].print(output); + output.print("}"); + } else { + output.print(self.segments[i]); + } + } + output.print("`"); + }); + AST_Arrow.DEFMETHOD("_do_print", function(output){ var self = this; var parent = output.parent(); diff --git a/lib/parse.js b/lib/parse.js index 92c2f261..7898a68a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -114,7 +114,7 @@ var WHITESPACE_CHARS = makePredicate(characters(" \u00a0\n\r\t\f\u000b\u200b\u18 var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); -var PUNC_CHARS = makePredicate(characters("[]{}(),;:")); +var PUNC_CHARS = makePredicate(characters("[]{}(),;:`")); var REGEXP_MODIFIERS = makePredicate(characters("gmsiy")); @@ -606,6 +606,9 @@ function tokenizer($TEXT, filename, html5_comments, shebang) { parse_error("Unexpected character '" + ch + "'"); }; + next_token.next = next; + next_token.peek = peek; + next_token.context = function(nc) { if (nc) S = nc; return S; @@ -817,6 +820,7 @@ function parse($TEXT, options) { }); case "[": case "(": + case "`": return simple_statement(); case ";": next(); @@ -1355,6 +1359,8 @@ function parse($TEXT, options) { return subscripts(array_(), allow_calls); case "{": return subscripts(object_or_object_destructuring_(), allow_calls); + case "`": + return subscripts(template_string(), allow_calls); } unexpected(); } @@ -1371,6 +1377,38 @@ function parse($TEXT, options) { unexpected(); }; + function template_string() { + var tokenizer_S = S.input, start = S.token, segments = [], segment = "", ch; + + while ((ch = tokenizer_S.next()) !== "`") { + if (ch === "$" && tokenizer_S.peek() === "{") { + segments.push(segment); segment = ""; + tokenizer_S.next(); + next(); + segments.push(expression()); + expect("}"); + if (is("punc", "`")) { + break; + } + continue; + } + segment += ch; + if (ch === "\\") { + segment += tokenizer_S.next(); + } + } + + segments.push(segment); + + next(); + + return new AST_TemplateString({ + start: start, + segments: segments, + end: S.token + }); + } + function expr_list(closing, allow_trailing_comma, allow_empty) { var first = true, a = []; while (!is("punc", closing)) { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2fcfd70f..f55c9868 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -46,6 +46,16 @@ typeof_arrow_functions: { expect_exact: "\"function\";" } +template_strings: { + input: { + ``; + `xx\`x`; + `${ foo + 2 }`; + ` foo ${ bar + `baz ${ qux }` }`; + } + expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; +} + destructuring_arguments: { input: { (function ( a ) { }); From 2a24fa8d2bc4ef5549b77defa3e046d3195ec582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 22:48:17 +0100 Subject: [PATCH 066/105] prefixed template strings, like "String.raw`foo\nbar`". --- lib/ast.js | 12 ++++++++++++ lib/output.js | 4 ++++ lib/parse.js | 7 +++++++ test/compress/harmony.js | 8 ++++++++ 4 files changed, 31 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index 6142bae2..756b1c69 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -501,6 +501,18 @@ var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { } }); +var AST_PrefixedTemplateString = DEFNODE("PrefixedTemplateString", "template_string prefix", { + $documentation: "A templatestring with a prefix, such as String.raw`foobarbaz`", + $propdoc: { + template_string: "[AST_TemplateString] The template string", + prefix: "[AST_SymbolRef|AST_PropAccess] The prefix, which can be a symbol such as `foo` or a dotted expression such as `String.raw`." + }, + _walk: function(visitor) { + this.prefix._walk(visitor); + this.template_string._walk(visitor); + } +}) + var AST_TemplateString = DEFNODE("TemplateString", "segments", { $documentation: "A template string literal", $propdoc: { diff --git a/lib/output.js b/lib/output.js index a9486bc2..da4bb540 100644 --- a/lib/output.js +++ b/lib/output.js @@ -799,6 +799,10 @@ function OutputStream(options) { self._do_print(output); }); + DEFPRINT(AST_PrefixedTemplateString, function(self, output) { + self.prefix.print(output); + self.template_string.print(output); + }); DEFPRINT(AST_TemplateString, function(self, output) { output.print("`"); for (var i = 0; i < self.segments.length; i++) { diff --git a/lib/parse.js b/lib/parse.js index 7898a68a..bae205ef 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1736,6 +1736,13 @@ function parse($TEXT, options) { }); return arrow_function(expr); } + if ((expr instanceof AST_SymbolRef || expr instanceof AST_PropAccess) && is("punc", "`")) { + return new AST_PrefixedTemplateString({ + start: start, + prefix: expr, + template_string: template_string() + }) + } if (commas && is("punc", ",")) { next(); return new AST_Seq({ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index f55c9868..930bb03e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -56,6 +56,14 @@ template_strings: { expect_exact: "``;`xx\\`x`;`${foo+2}`;` foo ${bar+`baz ${qux}`}`;"; } +template_string_prefixes: { + input: { + String.raw`foo`; + foo `bar`; + } + expect_exact: "String.raw`foo`;foo`bar`;"; +} + destructuring_arguments: { input: { (function ( a ) { }); From a92a14d1710a946214097fc6a5980e33c27516dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 5 Sep 2015 23:01:25 +0100 Subject: [PATCH 067/105] Remove unused state variable in_parameters, and also remove unreachable code (try_an_object always returned an object!) --- lib/parse.js | 134 ++++++++++++++------------------------- test/compress/harmony.js | 11 ++++ 2 files changed, 58 insertions(+), 87 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index bae205ef..e1de45c3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -687,7 +687,6 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, - in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -1060,7 +1059,6 @@ function parse($TEXT, options) { expect("("); var first = true; var a = []; - S.in_parameters = true; while (!is("punc", ")")) { if (first) first = false; else expect(","); if (is("expand", "...")) { @@ -1074,7 +1072,6 @@ function parse($TEXT, options) { a.push(expression(false)); } } - S.in_parameters = false; var end = S.token next(); return new AST_ArrowParametersOrSeq({ @@ -1432,97 +1429,60 @@ function parse($TEXT, options) { }); var object_or_object_destructuring_ = embed_tokens(function() { - var start = S.token; + var start = S.token, first = true, a = []; expect("{"); - function try_an_object() { - var first = true, a = []; - while (!is("punc", "}")) { - if (first) first = false; else expect(","); - if (!options.strict && is("punc", "}")) - // allow trailing comma - break; - var start = S.token; - var type = start.type; - var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - } - - if (!is("punc", ":")) { - // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectSymbol({ - start: start, - end: start, - symbol: new AST_SymbolRef({ - start: start, - end: start, - name: name - }) - })); - } else { - if (S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); - } - expect(":"); - a.push(new AST_ObjectKeyVal({ + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!options.strict && is("punc", "}")) + // allow trailing comma + break; + var start = S.token; + var type = start.type; + var name = as_property_name(); + if (type == "name" && !is("punc", ":")) { + if (name == "get") { + a.push(new AST_ObjectGetter({ start : start, - key : name, - value : expression(false), + key : as_atom_node(), + value : function_(AST_Accessor), end : prev() })); + continue; + } + if (name == "set") { + a.push(new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + })); + continue; } } - next(); - return new AST_Object({ properties: a }) + + if (!is("punc", ":")) { + // It's one of those object destructurings, the value is its own name + a.push(new AST_ObjectSymbol({ + start: start, + end: start, + symbol: new AST_SymbolRef({ + start: start, + end: start, + name: name + }) + })); + } else { + expect(":"); + a.push(new AST_ObjectKeyVal({ + start : start, + key : name, + value : expression(false), + end : prev() + })); + } } - - var obj = try_an_object(); - if (obj instanceof AST_Object) { return obj; } - - if (!S.in_parameters) { - croak("Cannot destructure", S.token.line, S.token.col); - } - - var firstName = obj; - - var namesInDestructuring = []; - - namesInDestructuring.push( new AST_SymbolRef({ - start : prev(), - end : prev(), - name : firstName - })); - - while (!is("punc", "}")) { - expect(","); - namesInDestructuring.push(as_symbol(AST_SymbolRef)) - } - - expect('}'); - - return new AST_Destructuring({ - start : start, - end : S.token, - names : namesInDestructuring, - is_array : false - }) + next(); + return new AST_Object({ properties: a }) }); function as_property_name() { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 930bb03e..ab0c73be 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -140,3 +140,14 @@ number_literals: { 9; } } + +// Fabio: My patches accidentally caused a crash whenever +// there's an extraneous set of parens around an object. +regression_cannot_destructure: { + input: { + var x = ({ x : 3 }); + x(({ x: 3 })); + } + expect_exact: "var x={x:3};x({x:3});"; +} + From 4ec5d8f644c46518ce984c36712437aa211f8cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 6 Sep 2015 21:33:17 +0100 Subject: [PATCH 068/105] parse, output the let statement --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 23 ++++++++++++++++++----- test/compress/block-scope.js | 8 ++++++++ 4 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 test/compress/block-scope.js diff --git a/lib/ast.js b/lib/ast.js index 756b1c69..94a96a64 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -682,6 +682,10 @@ var AST_Var = DEFNODE("Var", null, { $documentation: "A `var` statement" }, AST_Definitions); +var AST_Let = DEFNODE("Let", null, { + $documentation: "A `let` statement" +}, AST_Definitions); + var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); diff --git a/lib/output.js b/lib/output.js index da4bb540..463ca6ba 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1017,6 +1017,9 @@ function OutputStream(options) { if (!avoid_semicolon) output.semicolon(); }); + DEFPRINT(AST_Let, function(self, output){ + self._do_print(output, "let"); + }); DEFPRINT(AST_Var, function(self, output){ self._do_print(output, "var"); }); diff --git a/lib/parse.js b/lib/parse.js index e1de45c3..dbfa9f08 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var void while with'; +var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var let 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' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -891,6 +891,9 @@ function parse($TEXT, options) { case "var": return tmp = var_(), semicolon(), tmp; + case "let": + return tmp = let_(), semicolon(), tmp; + case "const": return tmp = const_(), semicolon(), tmp; @@ -961,13 +964,15 @@ function parse($TEXT, options) { expect("("); var init = null; if (!is("punc", ";")) { - init = is("keyword", "var") - ? (next(), var_(true)) - : expression(true, true); + init = + is("keyword", "var") ? (next(), var_(true)) : + is("keyword", "let") ? (next(), let_(true)) : + expression(true, true); var is_in = is("operator", "in"); var is_of = is("keyword", "of"); if (is_in || is_of) { - if (init instanceof AST_Var && init.definitions.length > 1) + if ((init instanceof AST_Var || init instanceof AST_Let) && + init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); next(); if (is_in) { @@ -1265,6 +1270,14 @@ function parse($TEXT, options) { }); }; + var let_ = function(no_in) { + return new AST_Let({ + start : prev(), + definitions : vardefs(no_in, false), + end : prev() + }); + }; + var const_ = function() { return new AST_Const({ start : prev(), diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js new file mode 100644 index 00000000..d0cdf75e --- /dev/null +++ b/test/compress/block-scope.js @@ -0,0 +1,8 @@ + +let_statement: { + input: { + let x = 6; + } + expect_exact: "let x=6;" +} + From bd3a9d3066da83df9c689ab20fe1b9df2b4fe4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 6 Sep 2015 21:56:55 +0100 Subject: [PATCH 069/105] Add a test to make sure future generations don't hoist lets --- test/compress/block-scope.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/compress/block-scope.js b/test/compress/block-scope.js index d0cdf75e..d1953ce5 100644 --- a/test/compress/block-scope.js +++ b/test/compress/block-scope.js @@ -6,3 +6,28 @@ let_statement: { expect_exact: "let x=6;" } +do_not_hoist_let: { + options = { + hoist_vars: true, + }; + input: { + function x() { + if (FOO) { + let let1; + let let2; + var var1; + var var2; + } + } + } + expect: { + function x() { + var var1, var2; + if (FOO) { + let let1; + let let2; + } + } + } +} + From bfeb1afad0f5229f8b739b62678d57e966019043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 7 Sep 2015 22:46:07 +0100 Subject: [PATCH 070/105] computed properties --- lib/ast.js | 10 ++++++++++ lib/compress.js | 3 +++ lib/output.js | 7 +++++++ lib/parse.js | 15 +++++++++++++++ test/compress/harmony.js | 7 +++++++ 5 files changed, 42 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index 94a96a64..ab3b2b62 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -921,6 +921,16 @@ var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { } }, AST_ObjectProperty); +var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { + $documentation: "An object property whose key is computed. Like `[Symbol.iterator]: function...` or `[routes.homepage]: renderHomepage`", + _walk: function(visitor) { + return visitor._visit(this, function(){ + this.key._walk(visitor); + this.value._walk(visitor); + }); + } +}, AST_ObjectProperty); + var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { $propdoc: { symbol: "[AST_SymbolRef] what symbol it is" diff --git a/lib/compress.js b/lib/compress.js index d7f9feda..ca817212 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1161,6 +1161,9 @@ merge(Compressor.prototype, { return false; }); def(AST_ObjectProperty, function(compressor){ + if (this instanceof AST_ObjectComputedKeyVal && + this.key.has_side_effects(compressor)) + return true; return this.value.has_side_effects(compressor); }); def(AST_Array, function(compressor){ diff --git a/lib/output.js b/lib/output.js index 463ca6ba..af7bf374 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1227,6 +1227,13 @@ function OutputStream(options) { self.key.print(output); self.value._do_print(output, true); }); + DEFPRINT(AST_ObjectComputedKeyVal, function(self, output) { + output.print("["); + self.key.print(output); + output.print("]:"); + output.space(); + self.value.print(output); + }); DEFPRINT(AST_Symbol, function(self, output){ var def = self.definition(); output.print_name(def ? def.mangled_name || def.name : self.name); diff --git a/lib/parse.js b/lib/parse.js index dbfa9f08..2ecdfb29 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1472,6 +1472,15 @@ function parse($TEXT, options) { continue; } } + + if (type == "punc" && start.value == "[") { + expect(":"); + a.push(new AST_ObjectComputedKeyVal({ + key: name, + value: expression(false) + })); + continue; + } if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name @@ -1502,6 +1511,12 @@ function parse($TEXT, options) { var tmp = S.token; next(); switch (tmp.type) { + case "punc": + if (tmp.value === "[") { + var ex = expression(false); + expect("]"); + return ex; + } else unexpected(); case "num": case "string": case "name": diff --git a/test/compress/harmony.js b/test/compress/harmony.js index ab0c73be..b763318e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,13 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +computed_property_names: { + input: { + obj({ ["x" + "x"]: 6 }); + } + expect_exact: "obj({[\"x\"+\"x\"]:6});" +} + typeof_arrow_functions: { options = { evaluate: true From 292c1317436ff64c78be3cd9221e4b5c47cd7cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 12 Oct 2015 21:39:19 +0100 Subject: [PATCH 071/105] Using single quotes --- test/compress/harmony.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index b763318e..86c2d33a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -40,7 +40,7 @@ computed_property_names: { input: { obj({ ["x" + "x"]: 6 }); } - expect_exact: "obj({[\"x\"+\"x\"]:6});" + expect_exact: 'obj({["x"+"x"]:6});' } typeof_arrow_functions: { From 5e1c89db44a1134a85ab200c9bc8f7e39be24a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 20:56:59 +0000 Subject: [PATCH 072/105] Allow 'of' to be a name. --- lib/parse.js | 4 ++-- test/compress/harmony.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 2ecdfb29..1e7ad954 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in of instanceof new return switch throw try typeof var let void while with'; +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 let 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' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -969,7 +969,7 @@ function parse($TEXT, options) { is("keyword", "let") ? (next(), let_(true)) : expression(true, true); var is_in = is("operator", "in"); - var is_of = is("keyword", "of"); + var is_of = is("name", "of"); if (is_in || is_of) { if ((init instanceof AST_Var || init instanceof AST_Let) && init.definitions.length > 1) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 86c2d33a..54be70d4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -158,3 +158,20 @@ regression_cannot_destructure: { expect_exact: "var x={x:3};x({x:3});"; } +regression_cannot_use_of: { + input: { + function of() { + } + var of = "is a valid variable name"; + of = { of: "is ok" }; + x.of; + of: foo() + } + expect: { + function of(){} + var of="is a valid variable name"; + of={of:"is ok"}; + x.of; + foo(); /* Label statement missing? No prob. */ + } +} From 8c05450ce06a38d045f8413cc8ae3da4fa11b61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 11 Oct 2015 18:22:07 +0100 Subject: [PATCH 073/105] start concise methods --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 10 ++++++++++ test/compress/harmony.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index ab3b2b62..680a2d53 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,6 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); +var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { + $documentation: "An ES6 concise method inside an object or class" +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); diff --git a/lib/output.js b/lib/output.js index af7bf374..41ab3be5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -844,6 +844,9 @@ function OutputStream(options) { } if (needs_parens) { output.print(")") } }); + DEFPRINT(AST_ConciseMethod, function(self, output){ + self._do_print(output, true /* do not print "function" */); + }); /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ diff --git a/lib/parse.js b/lib/parse.js index 1e7ad954..c5051071 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1453,6 +1453,16 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type == "name" && !is("punc", ":")) { + if (is("punc", "(")) { + a.push(new AST_ConciseMethod({ + start : start, + name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + })) + continue; + } if (name == "get") { a.push(new AST_ObjectGetter({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 54be70d4..718a547a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,22 @@ destructuring_arguments: { } } +concise_methods: { + input: { + x = { + foo(a, b) { + return x; + } + } + y = { + foo([{a}]) { + return a; + } + } + } + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" +} + number_literals: { input: { 0b1001; From e4f0db47edc7500fe72c75d6dd805584bd137db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 23 Oct 2015 18:59:07 +0100 Subject: [PATCH 074/105] Just making sure that concise methods are separated by commas. When classes come, they won't be necessary. --- test/compress/harmony.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 718a547a..19628be4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -142,10 +142,11 @@ concise_methods: { y = { foo([{a}]) { return a; - } + }, + bar(){} } } - expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } number_literals: { From 24b8e1203e922c5ef6677495acb01796789ebd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:14:55 +0000 Subject: [PATCH 075/105] Create a new symbol for methods' names --- lib/ast.js | 4 ++++ lib/parse.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 680a2d53..9014a5a6 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -991,6 +991,10 @@ var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { $documentation: "Symbol defining a function", }, AST_SymbolDeclaration); +var AST_SymbolMethod = DEFNODE("SymbolMethod", null, { + $documentation: "Symbol in an object defining a method", +}, AST_Symbol); + var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index c5051071..fe160a63 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1456,7 +1456,7 @@ function parse($TEXT, options) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, - name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), end : prev() From 858d763e6200d1010c33c23a20d56110ba55a55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:15:21 +0000 Subject: [PATCH 076/105] Make concise methods work with propmangle --- lib/propmangle.js | 8 ++++++++ test/compress/harmony.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/propmangle.js b/lib/propmangle.js index ff782b57..86da5de9 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -107,6 +107,9 @@ function mangle_properties(ast, options) { addStrings(node.property); } } + else if (node instanceof AST_ConciseMethod) { + add(node.name.name); + } })); // step 2: transform the tree, renaming properties @@ -129,6 +132,11 @@ function mangle_properties(ast, options) { else if (node instanceof AST_Sub) { node.property = mangleStrings(node.property); } + else if (node instanceof AST_ConciseMethod) { + if (should_mangle(node.name.name)) { + node.name.name = mangle(node.name.name); + } + } // else if (node instanceof AST_String) { // if (should_mangle(node.value)) { // AST_Node.warn( diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 19628be4..2525d252 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -149,6 +149,26 @@ concise_methods: { expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } +concise_methods_and_mangle_props: { + mangle_props = { + regex: /_/ + }; + input: { + function x() { + obj = { + _foo() { return 1; } + } + } + } + expect: { + function x() { + obj = { + a() { return 1; } + } + } + } +} + number_literals: { input: { 0b1001; From 44ecbdf5f0f84de343d86359b4456b38945d89bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 23:24:04 +0000 Subject: [PATCH 077/105] Accept keyword names as concise method names --- lib/parse.js | 2 +- test/compress/harmony.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index fe160a63..c9fb3a50 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1452,7 +1452,7 @@ function parse($TEXT, options) { var start = S.token; var type = start.type; var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { + if (type != "string" && type != "num" && !is("punc", ":")) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2525d252..731976f3 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -169,6 +169,18 @@ concise_methods_and_mangle_props: { } } +concise_methods_and_keyword_names: { + input: { + x = { + catch() {}, + throw() {} + } + } + expect: { + x={catch(){},throw(){}}; + } +} + number_literals: { input: { 0b1001; From a9a38ade1dc1e267e7c4b4ca4cbf94a7e57a56f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:40:46 +0000 Subject: [PATCH 078/105] Starting ES6 classes --- lib/ast.js | 12 +++++ lib/output.js | 31 ++++++++++++ lib/parse.js | 106 ++++++++++++++++++++++++++++----------- test/compress/harmony.js | 19 +++++++ 4 files changed, 140 insertions(+), 28 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 9014a5a6..b8759103 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -955,6 +955,14 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { $documentation: "An object getter property", }, AST_ObjectProperty); +var AST_Class = DEFNODE("Class", "name extends", { + $propdoc: { + name: "[AST_SymbolClassName?] optional class name.", + extends: "[AST_Node]? optional parent class", + }, + $documentation: "An ES6 class", +}, AST_Object); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -999,6 +1007,10 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); +var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { + $documentation: "Symbol naming a class's name. Lexically scoped to the class." +}, AST_SymbolDeclaration); + var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); diff --git a/lib/output.js b/lib/output.js index 41ab3be5..985347bb 100644 --- a/lib/output.js +++ b/lib/output.js @@ -510,6 +510,12 @@ function OutputStream(options) { return first_in_statement(output); }); + // Not a class, though. It can be alone in a statement although + // it extends from AST_Object. + PARENS(AST_Class, function() { + return false; + }); + PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; @@ -1200,6 +1206,31 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_Class, function(self, output){ + output.print("class"); + output.space(); + if (self.name) { + self.name.print(output); + output.space(); + } + if (self.extends) { + output.print("extends"); + output.space(); + self.extends.print(output); + output.space(); + } + if (self.properties.length > 0) output.with_block(function(){ + self.properties.forEach(function(prop, i){ + if (i) { + output.newline(); + } + output.indent(); + prop.print(output); + }); + output.newline(); + }); + else output.print("{}"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index c9fb3a50..78f4090b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -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 let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let 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 double enum export final float goto implements import int interface 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'; @@ -855,6 +855,9 @@ function parse($TEXT, options) { case "for": return for_(); + case "class": + return class_(); + case "function": return function_(AST_Defun); @@ -1381,6 +1384,13 @@ function parse($TEXT, options) { func.end = prev(); return subscripts(func, allow_calls); } + if (is("keyword", "class")) { + next(); + var cls = class_(); + cls.start = start; + cls.end = prev(); + return subscripts(cls, allow_calls); + } if (ATOMIC_START_TOKEN[S.token.type]) { return subscripts(as_atom_node(), allow_calls); } @@ -1453,32 +1463,9 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type != "string" && type != "num" && !is("punc", ":")) { - if (is("punc", "(")) { - a.push(new AST_ConciseMethod({ - start : start, - name : new AST_SymbolMethod({ name: name }), - argnames : params_or_seq_().as_params(croak), - body : _function_body(true), - end : prev() - })) - continue; - } - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); + var concise = concise_method_or_getset(name, start); + if (concise) { + a.push(concise); continue; } } @@ -1517,6 +1504,69 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); + function class_() { + var start, method, class_name, name, extends_, a = []; + + if (S.token.type == "name" && S.token.value != "extends") { + class_name = as_symbol(AST_SymbolClassName) + } + + if (S.token.value == "extends") { + next(); + extends_ = expression(true); + } + + expect("{"); + + if (is("punc", ";")) { next(); } // Leading semicolons are okay in class bodies. + while (!is("punc", "}")) { + start = S.token; + name = as_property_name(); + method = concise_method_or_getset(name, start); + if (!method) { croak(); } + a.push(method); + if (is("punc", ";")) { next(); } + } + + next(); + + return new AST_Class({ + start: start, + name: class_name, + extends: extends_, + properties: a, + end: prev(), + }); + } + + function concise_method_or_getset(name, start) { + if (is("punc", "(")) { + return new AST_ConciseMethod({ + start : start, + name : new AST_SymbolMethod({ name: name }), + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + }); + } + if (name == "get") { + return new AST_ObjectGetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + if (name == "set") { + return new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 731976f3..95e9bd54 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -181,6 +181,25 @@ concise_methods_and_keyword_names: { } } +classes: { + input: { + class SomeClass { + constructor() { + }; + foo() {}; + }; + class NoSemi { + constructor(...args) { + } + foo() {} + }; + class ChildClass extends SomeClass {}; + var asExpression = class AsExpression {}; + var nameless = class {}; + } + expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" +} + number_literals: { input: { 0b1001; From 7ea3ab6b58da0171cc12d8267e729ec6a4e24306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:51:47 +0000 Subject: [PATCH 079/105] static properties --- lib/ast.js | 15 ++++++++++++--- lib/output.js | 16 +++++++++++++++- lib/parse.js | 9 +++++++++ test/compress/harmony.js | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index b8759103..ed197a03 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,7 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); -var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { +var AST_ConciseMethod = DEFNODE("ConciseMethod", "static", { + $propdoc: { + static: "[boolean] whether this method is static (classes only)", + }, $documentation: "An ES6 concise method inside an object or class" }, AST_Lambda); @@ -947,11 +950,17 @@ var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { } }, AST_ObjectProperty); -var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { +var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static setter (classes only)" + }, $documentation: "An object setter property", }, AST_ObjectProperty); -var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { +var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static getter (classes only)" + }, $documentation: "An object getter property", }, AST_ObjectProperty); diff --git a/lib/output.js b/lib/output.js index 985347bb..0792dc54 100644 --- a/lib/output.js +++ b/lib/output.js @@ -787,9 +787,11 @@ function OutputStream(options) { var self = this; if (!nokeyword) { output.print("function"); + if (self.name) { + output.space(); + } } if (self.name) { - output.space(); self.name.print(output); } output.with_parens(function(){ @@ -851,6 +853,10 @@ function OutputStream(options) { if (needs_parens) { output.print(")") } }); DEFPRINT(AST_ConciseMethod, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } self._do_print(output, true /* do not print "function" */); }); @@ -1250,12 +1256,20 @@ function OutputStream(options) { self.value.print(output); }); DEFPRINT(AST_ObjectSetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("set"); output.space(); self.key.print(output); self.value._do_print(output, true); }); DEFPRINT(AST_ObjectGetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("get"); output.space(); self.key.print(output); diff --git a/lib/parse.js b/lib/parse.js index 78f4090b..17c1fe0f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1540,9 +1540,16 @@ function parse($TEXT, options) { } function concise_method_or_getset(name, start) { + var is_static = false; + if (name === "static" && !is("punc", "(")) { + is_static = true; + name = S.token.value; + next(); + } if (is("punc", "(")) { return new AST_ConciseMethod({ start : start, + static : is_static, name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), @@ -1552,6 +1559,7 @@ function parse($TEXT, options) { if (name == "get") { return new AST_ObjectGetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() @@ -1560,6 +1568,7 @@ function parse($TEXT, options) { if (name == "set") { return new AST_ObjectSetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 95e9bd54..e1b3078a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -200,6 +200,20 @@ classes: { expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" } +class_statics: { + input: { + x = class { + static staticMethod() {} + static get foo() {} + static set bar() {} + static() { /* "static" can be a method name! */ } + get() { /* "get" can be a method name! */ } + set() { /* "set" can be a method name! */ } + } + } + expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" +} + number_literals: { input: { 0b1001; From 46158d5114f6b9a21ec404c5b7382faf839c967e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 20 Nov 2015 19:34:10 +0000 Subject: [PATCH 080/105] Make AST_Class inherit AST_Scope instead of AST_Object This is one of those days I'd love to use multiple inheritance. An AST_Class has lots of common with AST_Object, but unfortunately `instanceof AST_Scope` is used very, very much, and a class has its name inside its own special pocket scope. This compels me to make AST_Class inherit Scope instead. It looks like, although there is much in common with AST_Object, `instanceof AST_Object` seldom are made, perhaps because it is less often necessary to traverse an object than a scope. --- lib/ast.js | 18 ++++++++++++++++-- lib/output.js | 6 ------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index ed197a03..cea74372 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -964,13 +964,27 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { $documentation: "An object getter property", }, AST_ObjectProperty); -var AST_Class = DEFNODE("Class", "name extends", { +var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { name: "[AST_SymbolClassName?] optional class name.", extends: "[AST_Node]? optional parent class", + properties: "[AST_ObjectProperty*] array of properties" }, $documentation: "An ES6 class", -}, AST_Object); + _walk: function(visitor) { + return visitor._visit(this, function(){ + if (this.name) { + this.name._walk(visitor); + } + if (this.extends) { + this.extends._walk(visitor); + } + this.properties.forEach(function(prop){ + prop._walk(visitor); + }); + }); + }, +}, AST_Scope); var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { diff --git a/lib/output.js b/lib/output.js index 0792dc54..a758f2c8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -510,12 +510,6 @@ function OutputStream(options) { return first_in_statement(output); }); - // Not a class, though. It can be alone in a statement although - // it extends from AST_Object. - PARENS(AST_Class, function() { - return false; - }); - PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; From 7a2394f4178e17033e97ab3879413e8ffb651cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 09:17:32 +0000 Subject: [PATCH 081/105] Separate class expressions from class declarations and their symbols like defuns --- lib/ast.js | 16 ++++++++++++++-- lib/parse.js | 14 +++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index cea74372..f589fb43 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -966,7 +966,7 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { - name: "[AST_SymbolClassName?] optional class name.", + name: "[AST_SymbolClass|AST_SymbolDefClass?] optional class name.", extends: "[AST_Node]? optional parent class", properties: "[AST_ObjectProperty*] array of properties" }, @@ -986,6 +986,14 @@ var AST_Class = DEFNODE("Class", "name extends properties", { }, }, AST_Scope); +var AST_DefClass = DEFNODE("DefClass", null, { + $documentation: "A class definition", +}, AST_Class); + +var AST_ClassExpression = DEFNODE("ClassExpression", null, { + $documentation: "A class expression." +}, AST_Class); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -1030,7 +1038,11 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); -var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { +var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, { + $documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class." +}, AST_SymbolDeclaration); + +var AST_SymbolClass = DEFNODE("SymbolClass", null, { $documentation: "Symbol naming a class's name. Lexically scoped to the class." }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index 17c1fe0f..67575133 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -856,7 +856,7 @@ function parse($TEXT, options) { return for_(); case "class": - return class_(); + return class_(AST_DefClass); case "function": return function_(AST_Defun); @@ -1386,7 +1386,7 @@ function parse($TEXT, options) { } if (is("keyword", "class")) { next(); - var cls = class_(); + var cls = class_(AST_ClassExpression); cls.start = start; cls.end = prev(); return subscripts(cls, allow_calls); @@ -1504,11 +1504,15 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); - function class_() { + function class_(KindOfClass) { var start, method, class_name, name, extends_, a = []; if (S.token.type == "name" && S.token.value != "extends") { - class_name = as_symbol(AST_SymbolClassName) + class_name = as_symbol(KindOfClass === AST_DefClass ? AST_SymbolDefClass : AST_SymbolClass); + } + + if (KindOfClass === AST_DefClass && !class_name) { + croak(); } if (S.token.value == "extends") { @@ -1530,7 +1534,7 @@ function parse($TEXT, options) { next(); - return new AST_Class({ + return new KindOfClass({ start: start, name: class_name, extends: extends_, From 805e027ab8edbda224c30c0e7c29f4353d004f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 12:20:20 +0000 Subject: [PATCH 082/105] mangle class names --- lib/scope.js | 18 +++++++++++++++--- test/compress/harmony.js | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 2388fb52..369d04e1 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -165,9 +165,17 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // later. (node.scope = defun.parent_scope).def_function(node); } - else if (node instanceof AST_Var) { + else if (node instanceof AST_Var) { last_var_had_const_pragma = node.has_const_pragma(); } + else if (node instanceof AST_SymbolClass) { + defun.def_variable(node); + } + else if (node instanceof AST_SymbolDefClass) { + // This deals with the name of the class being available + // inside the class. + (node.scope = defun.parent_scope).def_function(node); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); @@ -193,6 +201,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // pass 2: find back references and eval var func = null; + var cls = null; var globals = self.globals = new Dictionary(); var tw = new TreeWalker(function(node, descend){ if (node instanceof AST_Lambda) { @@ -202,8 +211,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ func = prev_func; return true; } - if (node instanceof AST_LoopControl && node.label) { - node.label.thedef.references.push(node); + if (node instanceof AST_Class) { + var prev_cls = cls; + cls = node; + descend(); + cls = prev_cls; return true; } if (node instanceof AST_SymbolRef) { diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e1b3078a..c959b5d1 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -214,6 +214,25 @@ class_statics: { expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" } +class_name_can_be_mangled: { + mangle = { }; + input: { + function x() { + class Foo { + } + var class1 = Foo + var class2 = class Bar {} + } + } + expect: { + function x() { + class a { } + var b = a + var c = class a {} + } + } +} + number_literals: { input: { 0b1001; From 89dfbfd46be7c2f59a3b030fe3aa44cb19e26ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 13:59:18 +0000 Subject: [PATCH 083/105] Mangle class names correctly --- lib/scope.js | 9 +++++++-- test/compress/harmony.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 369d04e1..b15d72e4 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -66,7 +66,11 @@ SymbolDef.prototype = { || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) || (options.keep_fnames && (this.orig[0] instanceof AST_SymbolLambda - || this.orig[0] instanceof AST_SymbolDefun)); + || this.orig[0] instanceof AST_SymbolDefun)) + || this.orig[0] instanceof AST_SymbolMethod + || (options.keep_classnames + && (this.orig[0] instanceof AST_SymbolClass + || this.orig[0] instanceof AST_SymbolDefClass)); }, mangle: function(options) { var cache = options.cache && options.cache.props; @@ -416,7 +420,8 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ sort : false, // Ignored. Flag retained for backwards compatibility. toplevel : false, screw_ie8 : false, - keep_fnames : false + keep_fnames : false, + keep_classnames : false }); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c959b5d1..1d18301d 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -233,6 +233,24 @@ class_name_can_be_mangled: { } } +class_name_can_be_preserved: { + mangle = { + keep_classnames: true + } + input: { + function x() { + (class Baz { }); + class Foo {}; + } + } + expect: { + function x() { + (class Baz { }); + class Foo {}; + } + } +} + number_literals: { input: { 0b1001; From 75276995896bc52ab38849c21031b9ee5abfb735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 14:48:23 +0000 Subject: [PATCH 084/105] Implement new.target --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 8 ++++++++ test/compress/harmony.js | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index f589fb43..d1b46f91 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1003,6 +1003,10 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $documentation: "Base class for all symbols", }); +var AST_NewTarget = DEFNODE("NewTarget", null, { + $documentation: "A reference to new.target" +}); + var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); diff --git a/lib/output.js b/lib/output.js index a758f2c8..702aa317 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1231,6 +1231,9 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_NewTarget, function(self, output) { + output.print("new.target"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index 67575133..c5e8c0fe 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1292,6 +1292,14 @@ function parse($TEXT, options) { var new_ = function(allow_calls) { var start = S.token; expect_token("operator", "new"); + if (is("punc", ".")) { + next(); + expect_token("name"); + return subscripts(new AST_NewTarget({ + start : start, + end : prev() + }), allow_calls); + } var newexp = expr_atom(false), args; if (is("punc", "(")) { next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 1d18301d..e53f9458 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -251,6 +251,14 @@ class_name_can_be_preserved: { } } +new_target: { + input: { + new.target; + new.target.name; + } + expect_exact: "new.target;new.target.name;" +} + number_literals: { input: { 0b1001; From 5b4490d82e5b752c3bfe83388faebb36d2468ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 085/105] Non-destructuring default parameters --- lib/ast.js | 17 ++++++++++++++++- lib/output.js | 18 +++++++++++++++--- test/compress/harmony.js | 8 ++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index d1b46f91..9337ef3c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,6 +422,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { is_array: true, names: ex.elements.map(to_fun_args) }); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -1026,8 +1033,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 702aa317..fafcbaf9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1279,9 +1279,21 @@ function OutputStream(options) { output.space(); self.value.print(output); }); - DEFPRINT(AST_Symbol, function(self, output){ - var def = self.definition(); - output.print_name(def ? def.mangled_name || def.name : self.name); + AST_Symbol.DEFMETHOD("_do_print", function(output){ + var def = this.definition(); + output.print_name(def ? def.mangled_name || def.name : this.name); + }); + DEFPRINT(AST_Symbol, function (self, output) { + self._do_print(output); + }); + DEFPRINT(AST_SymbolFunarg, function(self, output){ + self._do_print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }); DEFPRINT(AST_ObjectSymbol, function(self, output){ var name = self.mangled_key || self.symbol.name; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e53f9458..a1f2aa9c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,14 @@ destructuring_arguments: { } } +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}" +} + concise_methods: { input: { x = { From 752a7affc8e5d427c23152beee51d5028e50f854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 086/105] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 16 +++++++--------- lib/output.js | 6 ++++++ test/compress/harmony.js | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 9337ef3c..5a26e237 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -384,7 +384,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring var root = this; - return this.expressions.map(function to_fun_args(ex) { + return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { if (ex.properties.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -392,6 +392,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: false, + default: default_seen_above, names: ex.properties.map(to_fun_args) }); } else if (ex instanceof AST_ObjectSymbol) { @@ -408,6 +409,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, + default: default_seen_above, start: ex.start, end: ex.end }); @@ -420,15 +422,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: true, + default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -492,7 +490,7 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index fafcbaf9..066c1432 100644 --- a/lib/output.js +++ b/lib/output.js @@ -643,6 +643,12 @@ function OutputStream(options) { name.print(output); }) output.print(self.is_array ? "]" : "}"); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }) DEFPRINT(AST_Debugger, function(self, output){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index a1f2aa9c..69cf4caa 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -136,8 +136,9 @@ default_arguments: { input: { function x(a = 6) { } function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } } - expect_exact: "function x(a=6){}function x(a=6+5){}" + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } concise_methods: { From e6b005fd37fe335ab1606f15baa33c5716d3357d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 087/105] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 20 +++++++++----------- lib/output.js | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 5a26e237..5f34dc5b 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1016,10 +1016,16 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." + init: "[AST_Node*/S] array of initializers for this declaration.", + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); } }, AST_Symbol); @@ -1031,16 +1037,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 066c1432..0cb387ce 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1292,7 +1292,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 6c87242d632b5f2f401a1c94791145b62fb42ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:04:42 +0000 Subject: [PATCH 088/105] Default values inside destructurings --- lib/parse.js | 24 +++++++++++++++++++++--- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index c5e8c0fe..0c22bb2d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1252,8 +1252,12 @@ function parse($TEXT, options) { })); next(); } else if (is("name")) { - children.push(_make_symbol(sym_type)); - next(); + children.push(new (sym_type)({ + name : String(S.token.value), + start : S.token, + default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, + end : S.token + })); } else { children.push(expression()); } @@ -1487,7 +1491,21 @@ function parse($TEXT, options) { continue; } - if (!is("punc", ":")) { + if (is("operator", "=")) { + next(); + a.push(new AST_Assign({ + start: start, + // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. + left: new AST_SymbolRef({ + start: start, + end: start, + name: name + }), + operator: "=", + right: expression(false), + end: prev() + })); + } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectSymbol({ start: start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 69cf4caa..c3f48b42 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -141,6 +141,16 @@ default_arguments: { expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + concise_methods: { input: { x = { From 166f1138bcd2bf1e857039cf970f52544ed47cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 089/105] Non-destructuring default parameters --- lib/ast.js | 19 ++++++++++++++++--- lib/output.js | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 5f34dc5b..ae10abdf 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -425,8 +425,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign) { - return to_fun_args(ex.left, undefined, undefined, ex.right); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -1037,8 +1042,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 0cb387ce..066c1432 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1292,7 +1292,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolDeclaration, function(self, output){ + DEFPRINT(AST_SymbolFunarg, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 600ff2f6beccba1daaf0c30a930f8461273bea15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 090/105] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index ae10abdf..7ae3a114 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -425,13 +425,8 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } From 6a95d60abd0518db62417101d244327971c81225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 091/105] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 10 +--------- lib/output.js | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 7ae3a114..5f34dc5b 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1037,16 +1037,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 066c1432..0cb387ce 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1292,7 +1292,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From e457f7aab56262e40a093c2ff3244cd585cb8967 Mon Sep 17 00:00:00 2001 From: Fugiman Date: Mon, 18 Jan 2016 21:28:26 -0800 Subject: [PATCH 092/105] Fix template string parsing --- lib/parse.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 0c22bb2d..04ec692d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1418,10 +1418,8 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - expect("}"); - if (is("punc", "`")) { - break; - } + if (!is("punc", "}")) + token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); continue; } segment += ch; From de5fb76d942ccfa4813905985b8701f46a8081c9 Mon Sep 17 00:00:00 2001 From: Fugiman Date: Tue, 19 Jan 2016 02:04:17 -0800 Subject: [PATCH 093/105] Remove duplicate error message --- lib/parse.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 04ec692d..3dd7f3e2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1418,8 +1418,10 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - if (!is("punc", "}")) - token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); + if (!is("punc", "}")) { + // force error message + expect("}"); + } continue; } segment += ch; From 7697be14e7b2f7d5c2a090498102e273353633f8 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 19 Jan 2016 19:21:53 +0100 Subject: [PATCH 094/105] Add test for bad template string parsing --- test/compress/issue-926.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/compress/issue-926.js diff --git a/test/compress/issue-926.js b/test/compress/issue-926.js new file mode 100644 index 00000000..e717efb0 --- /dev/null +++ b/test/compress/issue-926.js @@ -0,0 +1,9 @@ +template_strings: { + input: { + foo( + `${contents}`, + `${text}` + ); + } + expect_exact: "foo(`${contents}`,`${text}`);" +} From c5cf25e077cef9ac0db16d791251983217445e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 9 Feb 2016 00:02:23 +0000 Subject: [PATCH 095/105] Fix #931: Create arrow functions in maybe_assign so that they can be used in assignments --- lib/parse.js | 21 ++++++++++++++++++++- test/compress/harmony.js | 11 +++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 3dd7f3e2..bab4c21b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1029,7 +1029,12 @@ function parse($TEXT, options) { var arrow_function = function(args) { expect_token("arrow", "=>"); - var argnames = args.as_params(croak); + var argnames; + if (typeof args.length === 'number') { + argnames = args; + } else { + argnames = args.as_params(croak); + } var body = is("punc", "{") ? _function_body(true) : @@ -1793,6 +1798,12 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + if (start.value == "(" && peek().value == ")") { + next(); + next(); + return arrow_function([]); + } + var left = maybe_conditional(no_in); var val = S.token.value; @@ -1809,6 +1820,14 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } + if (is("arrow")) { + left = new AST_SymbolFunarg({ + name: left.name, + start: left.start, + end: left.end, + }); + return arrow_function([left]) + } return left; }; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c3f48b42..622e00a0 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,17 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +regression_assign_arrow_functions: { + input: { + oninstall = e => false; + oninstall = () => false; + } + expect: { + oninstall=e=>false; + oninstall=()=>false; + } +} + computed_property_names: { input: { obj({ ["x" + "x"]: 6 }); From 627083fa16f99a7085cdc9e66fb1e0bcb248b95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Thu, 4 Feb 2016 21:54:21 +0000 Subject: [PATCH 096/105] fix: don't fail if definition is undefined Running `uglifyjs --verbose --compress --mangle --screw-ie8 class.js` with `class.js`: ``` class Foo { bar() { } } ``` Fails with: ``` undefined:4041 return this.definition().unmangleable(options); TypeError: Cannot read property 'unmangleable' of undefined ... ``` --- lib/scope.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index b15d72e4..b2a073e6 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -369,7 +369,8 @@ AST_Scope.DEFMETHOD("references", function(sym){ }); AST_Symbol.DEFMETHOD("unmangleable", function(options){ - return this.definition().unmangleable(options); + var def = this.definition(); + return def && def.unmangleable(options); }); // property accessors are not mangleable From 0e230169f815e3f4d0158d2a9eaf4f1cae4c984b Mon Sep 17 00:00:00 2001 From: viclm Date: Tue, 8 Mar 2016 15:43:01 +0800 Subject: [PATCH 097/105] Fix eager parsing of arrow functions for non-punc tokens --- lib/parse.js | 2 +- test/compress/issue-1001.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1001.js diff --git a/lib/parse.js b/lib/parse.js index bab4c21b..2214de71 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1798,7 +1798,7 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; - if (start.value == "(" && peek().value == ")") { + if (start.type == "punc" && start.value == "(" && peek().value == ")") { next(); next(); return arrow_function([]); diff --git a/test/compress/issue-1001.js b/test/compress/issue-1001.js new file mode 100644 index 00000000..89adf81c --- /dev/null +++ b/test/compress/issue-1001.js @@ -0,0 +1,8 @@ +parenthesis_strings_in_parenthesis: { + input: { + ('('); + a(')'); + + } + expect_exact: '"(";a(")");' +} From 5f511fe34f58bff4b20e150d1d599c5a83c1976b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 29 Jan 2016 20:47:49 +0000 Subject: [PATCH 098/105] Starting out the import statement --- lib/ast.js | 7 +++++++ lib/output.js | 6 ++++++ lib/parse.js | 20 ++++++++++++++++++-- test/compress/harmony.js | 8 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 5f34dc5b..41ab5f64 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,6 +702,13 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); +var AST_Import = DEFNODE("Import", "module_name", { + $documentation: "An `import` statement", + $propdoc: { + module_name: "[AST_String] String literal describing where this module came from", + } +}); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index 0cb387ce..2e8d39a6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1041,6 +1041,12 @@ function OutputStream(options) { DEFPRINT(AST_Const, function(self, output){ self._do_print(output, "const"); }); + DEFPRINT(AST_Import, function(self, output) { + output.print("import"); + output.space(); + self.module_name.print(output); + output.semicolon(); + }); function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); diff --git a/lib/parse.js b/lib/parse.js index 2214de71..d05b7ef1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char double enum export 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 double enum export final float goto implements int interface 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'; @@ -906,6 +906,9 @@ function parse($TEXT, options) { body : statement() }); + case "import": + return tmp = import_(), semicolon(), tmp; + default: unexpected(); } @@ -1611,6 +1614,19 @@ function parse($TEXT, options) { } } + function import_() { + return new AST_Import({ + start: prev(), + module_name: new AST_String({ + start : S.token, + value : S.token.value, + quote : S.token.quote, + end : S.token, + }), + end: next(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 622e00a0..4cb3921c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -305,6 +305,14 @@ number_literals: { } } +import_statement: { + input: { + import "mod-name"; + import "module2"; + } + expect_exact: "import\"mod-name\";import\"module2\";" +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From b8adc79433697eeccd0db0ed4d88fcf1684b0426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 21 Feb 2016 17:06:09 +0000 Subject: [PATCH 099/105] Importing names from places --- lib/ast.js | 15 ++++++++++++++- lib/compress.js | 4 ++++ lib/output.js | 6 ++++++ lib/parse.js | 24 ++++++++++++++++++------ test/compress/harmony.js | 16 ++++++++++++++-- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 41ab5f64..a56fba2e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "module_name", { +var AST_Import = DEFNODE("Import", "imported_name module_name", { $documentation: "An `import` statement", $propdoc: { + imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", module_name: "[AST_String] String literal describing where this module came from", + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + if (this.imported_name) { + this.imported_name._walk(visitor); + } + this.module_name._walk(visitor); + }); } }); @@ -1072,6 +1081,10 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); +var AST_SymbolImport = DEFNODE("SymbolImport", null, { + $documentation: "Symbol refering to an imported name", +}, AST_SymbolDeclaration); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/compress.js b/lib/compress.js index ca817212..bb77f6a6 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1965,6 +1965,10 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_Import, function(self, compressor) { + return self; + }); + OPT(AST_Function, function(self, compressor){ self = AST_Lambda.prototype.optimize.call(self, compressor); if (compressor.option("unused") && !compressor.option("keep_fnames")) { diff --git a/lib/output.js b/lib/output.js index 2e8d39a6..ea59e080 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1044,6 +1044,12 @@ function OutputStream(options) { DEFPRINT(AST_Import, function(self, output) { output.print("import"); output.space(); + if (self.imported_name) { + self.imported_name.print(output); + output.space(); + output.print("from") + output.space(); + } self.module_name.print(output); output.semicolon(); }); diff --git a/lib/parse.js b/lib/parse.js index d05b7ef1..eb42e15f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1615,15 +1615,27 @@ function parse($TEXT, options) { } function import_() { + var start = prev(); + var imported_name; + if (is("name")) { + imported_name = as_symbol(AST_SymbolImport); + expect_token("name", "from"); + } + var mod_str = S.token; + if (mod_str.type !== 'string') { + unexpected(); + } + next(); return new AST_Import({ - start: prev(), + start: start, + imported_name: imported_name, module_name: new AST_String({ - start : S.token, - value : S.token.value, - quote : S.token.quote, - end : S.token, + start: mod_str, + value: mod_str.value, + quote: mod_str.quote, + end: mod_str, }), - end: next(), + end: S.token, }); } diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 4cb3921c..dc1c2e4f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -308,9 +308,21 @@ number_literals: { import_statement: { input: { import "mod-name"; - import "module2"; + import Foo from "bar"; + } + expect_exact: "import\"mod-name\";import Foo from\"bar\";" +} + +import_statement_mangling: { + mangle = { }; + input: { + import Foo from "foo"; + Foo(); + } + expect: { + import a from "foo"; + a(); } - expect_exact: "import\"mod-name\";import\"module2\";" } // Fabio: My patches accidentally caused a crash whenever From 07fde71cac174fa63bd774d08be65f36bd27372f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 26 Feb 2016 21:12:19 +0000 Subject: [PATCH 100/105] importing names in the modules, not just default imports --- lib/ast.js | 15 ++++++++++++++- lib/output.js | 31 +++++++++++++++++++++++++++++++ lib/parse.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/compress/harmony.js | 5 ++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index a56fba2e..1358603d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "imported_name module_name", { +var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { + $documentation: "The part of the import statement that imports names from a module.", + $propdoc: { + foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", + name: "[AST_SymbolImport] The name as it becomes available to this module." + } +}) + +var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { $documentation: "An `import` statement", $propdoc: { imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", + imported_names: "[AST_NameImport*] The names of non-default imported variables", module_name: "[AST_String] String literal describing where this module came from", }, _walk: function(visitor) { @@ -1085,6 +1094,10 @@ var AST_SymbolImport = DEFNODE("SymbolImport", null, { $documentation: "Symbol refering to an imported name", }, AST_SymbolDeclaration); +var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { + $documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes", +}, AST_Symbol); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/output.js b/lib/output.js index ea59e080..ebce1d8b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1046,6 +1046,25 @@ function OutputStream(options) { output.space(); if (self.imported_name) { self.imported_name.print(output); + } + if (self.imported_name && self.imported_names) { + output.print(","); + output.space(); + } + if (self.imported_names) { + output.print("{"); + self.imported_names.forEach(function(name_import, i) { + output.space(); + name_import.print(output); + if (i < self.imported_names.length - 1) { + output.print(","); + output.space(); + } + }); + output.space(); + output.print("}"); + } + if (self.imported_name || self.imported_names) { output.space(); output.print("from") output.space(); @@ -1054,6 +1073,18 @@ function OutputStream(options) { output.semicolon(); }); + DEFPRINT(AST_NameImport, function(self, output) { + if (self.foreign_name) { + self.foreign_name.print(output); + output.space(); + output.print("as"); + output.space(); + self.name.print(output); + } else { + self.name.print(output); + } + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index eb42e15f..98843231 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1617,8 +1617,28 @@ function parse($TEXT, options) { function import_() { var start = prev(); var imported_name; + var imported_names; if (is("name")) { imported_name = as_symbol(AST_SymbolImport); + } + + if (is("punc", ",")) { + next(); + } + + if (is("punc", "{")) { + next(); + imported_names = []; + while (!is("punc", "}")) { + imported_names.push(import_name()); + if (is("punc", ",")) { + next(); + } + } + next(); + } + + if (imported_names || imported_name) { expect_token("name", "from"); } var mod_str = S.token; @@ -1629,6 +1649,7 @@ function parse($TEXT, options) { return new AST_Import({ start: start, imported_name: imported_name, + imported_names: imported_names, module_name: new AST_String({ start: mod_str, value: mod_str.value, @@ -1639,6 +1660,25 @@ function parse($TEXT, options) { }); } + function import_name() { + var start = S.token; + var foreign_name; + var name; + + if (peek().value === "as" && peek().type === "name") { + foreign_name = as_symbol(AST_SymbolImportForeign); + next(); // The "as" word + } + name = as_symbol(AST_SymbolImport); + + return new AST_NameImport({ + start: start, + foreign_name: foreign_name, + name: name, + end: prev(), + }) + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index dc1c2e4f..59c6238a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -309,8 +309,11 @@ import_statement: { input: { import "mod-name"; import Foo from "bar"; + import { Bar, Baz } from 'lel'; + import Bar, { Foo } from 'lel'; + import { Bar as kex, Baz as food } from 'lel'; } - expect_exact: "import\"mod-name\";import Foo from\"bar\";" + expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } import_statement_mangling: { From 7b3e4d4fb267caad15802fdde0e09040ed24f22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:01:16 +0000 Subject: [PATCH 101/105] Mangling externally imported names by using aliasing --- lib/ast.js | 11 +++++++++++ lib/output.js | 8 ++++++-- lib/parse.js | 8 ++++++++ lib/scope.js | 9 +++++++++ test/compress/harmony.js | 10 ++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 1358603d..19f8753f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -707,6 +707,12 @@ var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { $propdoc: { foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", name: "[AST_SymbolImport] The name as it becomes available to this module." + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + this.foreign_name._walk(visitor); + this.name._walk(visitor); + }); } }) @@ -722,6 +728,11 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { if (this.imported_name) { this.imported_name._walk(visitor); } + if (this.imported_names) { + this.imported_names.forEach(function (name_import) { + name_import._walk(visitor); + }); + } this.module_name._walk(visitor); }); } diff --git a/lib/output.js b/lib/output.js index ebce1d8b..319e2b05 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1074,8 +1074,12 @@ function OutputStream(options) { }); DEFPRINT(AST_NameImport, function(self, output) { - if (self.foreign_name) { - self.foreign_name.print(output); + var definition = self.name.definition(); + var names_are_different = + (definition && definition.mangled_name || self.name.name) !== + self.foreign_name.name; + if (names_are_different) { + output.print(self.foreign_name.name); output.space(); output.print("as"); output.space(); diff --git a/lib/parse.js b/lib/parse.js index 98843231..c2b32237 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1671,6 +1671,14 @@ function parse($TEXT, options) { } name = as_symbol(AST_SymbolImport); + if (foreign_name === undefined) { + foreign_name = new AST_SymbolImportForeign({ + name: name.name, + start: name.start, + end: name.end, + }); + } + return new AST_NameImport({ start: start, foreign_name: foreign_name, diff --git a/lib/scope.js b/lib/scope.js index b2a073e6..789865af 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -175,6 +175,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolClass) { defun.def_variable(node); } + else if (node instanceof AST_SymbolImport) { + scope.def_variable(node); + } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. @@ -321,6 +324,12 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ this.variables.set(symbol.name, def); def.object_destructuring_arg = symbol.object_destructuring_arg; def.global = !this.parent_scope; + if (symbol instanceof AST_SymbolImport) { + // Imports are not global + def.global = false; + // TODO The real fix comes with block scoping being first class in uglifyJS, + // enabling import definitions to behave like module-level let declarations + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 59c6238a..5ef1416e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -320,11 +320,21 @@ import_statement_mangling: { mangle = { }; input: { import Foo from "foo"; + import Bar, {Food} from "lel"; + import {What as Whatever} from "lel"; Foo(); + Bar(); + Food(); + Whatever(); } expect: { import a from "foo"; + import b, {Food as c} from "lel"; + import {What as d} from "lel"; a(); + b(); + c(); + d(); } } From 59c66fc2e1c737d97904e42b912e915a7fec5b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:24:18 +0000 Subject: [PATCH 102/105] Implement the export statement --- lib/ast.js | 9 +++++++++ lib/output.js | 15 +++++++++++++++ lib/parse.js | 35 ++++++++++++++++++++++++++++++++++- test/compress/harmony.js | 12 ++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 19f8753f..2acf5e6b 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -738,6 +738,15 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { } }); +var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default", { + $documentation: "An `export` statement", + $propdoc: { + exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition", + exported_value: "[AST_Node?] An exported value", + is_default: "[Boolean] Whether this is the default exported value of this module" + }, +}, AST_Statement); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index 319e2b05..35b4816f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1089,6 +1089,21 @@ function OutputStream(options) { } }); + DEFPRINT(AST_Export, function(self, output) { + output.print("export"); + output.space(); + if (self.is_default) { + output.print("default"); + output.space(); + } + if (self.exported_value) { + self.exported_value.print(output); + } else if (self.exported_definition) { + self.exported_definition.print(output); + } + output.semicolon(); + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index c2b32237..4f33f4b6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import export'; var KEYWORDS_ATOM = 'false null true'; var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -909,6 +909,9 @@ function parse($TEXT, options) { case "import": return tmp = import_(), semicolon(), tmp; + case "export": + return tmp = export_(), semicolon(), tmp; + default: unexpected(); } @@ -1687,6 +1690,36 @@ function parse($TEXT, options) { }) } + function export_() { + var start = S.token; + var is_default; + var exported_value; + var exported_definition; + + if (is("keyword", "default")) { + is_default = true; + next(); + } + + var is_definition = + is("keyword", "var") || is("keyword", "let") || is("keyword", "const") || + is("keyword", "class") || is("keyword", "function"); + + if (is_definition) { + exported_definition = statement(); + } else { + exported_value = expression(); + } + + return new AST_Export({ + start: start, + is_default: is_default, + exported_value: exported_value, + exported_definition: exported_definition, + end: prev(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 5ef1416e..d27d903b 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -316,6 +316,18 @@ import_statement: { expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } +export_statement: { + input: { + export default 1; + export var foo = 4; + export let foo = 6; + export const foo = 6; + export function foo() {}; + export class foo { }; + } + expect_exact: "export default 1;export var foo=4;export let foo=6;export const foo=6;export function foo(){};export class foo{};" +} + import_statement_mangling: { mangle = { }; input: { From a3f094013db0597bb46acbf9bd3c31ea17bf48c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:40:57 +0000 Subject: [PATCH 103/105] Don't mangle exported symbols --- lib/ast.js | 10 ++++++++++ lib/scope.js | 28 ++++++++++++++++++++-------- test/compress/harmony.js | 16 ++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 2acf5e6b..71459770 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -745,6 +745,16 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul exported_value: "[AST_Node?] An exported value", is_default: "[Boolean] Whether this is the default exported value of this module" }, + _walk: function (visitor) { + visitor._visit(this, function () { + if (this.exported_definition) { + this.exported_definition._walk(visitor); + } + if (this.exported_value) { + this.exported_value._walk(visitor); + } + }); + } }, AST_Statement); var AST_VarDef = DEFNODE("VarDef", "name value", { diff --git a/lib/scope.js b/lib/scope.js index 789865af..5de7aa23 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -49,6 +49,7 @@ function SymbolDef(scope, index, orig) { this.scope = scope; this.references = []; this.global = false; + this.export = false; this.mangled_name = null; this.object_destructuring_arg = false; this.undeclared = false; @@ -61,6 +62,7 @@ SymbolDef.prototype = { if (!options) options = {}; return (this.global && !options.toplevel) + || this.export || this.object_destructuring_arg || this.undeclared || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) @@ -103,6 +105,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var last_var_had_const_pragma = false; var nesting = 0; var in_destructuring = null; + var in_export; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -132,6 +135,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ labels = save_labels; return true; // don't descend again in TreeWalker } + if (node instanceof AST_Export) { + in_export = true; + descend(); + in_export = false; + } if (node instanceof AST_LabeledStatement) { var l = node.label; if (labels.has(l.name)) { @@ -156,10 +164,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolFunarg) { node.object_destructuring_arg = !!in_destructuring; - defun.def_variable(node); + defun.def_variable(node, in_export); } if (node instanceof AST_SymbolLambda) { - defun.def_function(node); + defun.def_function(node, in_export); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -167,25 +175,25 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_Var) { last_var_had_const_pragma = node.has_const_pragma(); } else if (node instanceof AST_SymbolClass) { - defun.def_variable(node); + defun.def_variable(node, in_export); } else if (node instanceof AST_SymbolImport) { - scope.def_variable(node); + scope.def_variable(node, in_export); } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node); + var def = defun.def_variable(node, in_export); def.constant = node instanceof AST_SymbolConst; def.destructuring = in_destructuring; def.init = tw.parent().value; @@ -317,7 +325,7 @@ AST_Scope.DEFMETHOD("def_function", function(symbol){ this.functions.set(symbol.name, this.def_variable(symbol)); }); -AST_Scope.DEFMETHOD("def_variable", function(symbol){ +AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){ var def; if (!this.variables.has(symbol.name)) { def = new SymbolDef(this, this.variables.size(), symbol); @@ -330,6 +338,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ // TODO The real fix comes with block scoping being first class in uglifyJS, // enabling import definitions to behave like module-level let declarations } + if (!this.parent_scope && in_export) { + def.global = false; + def.export = true; + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index d27d903b..fec3a835 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -350,6 +350,22 @@ import_statement_mangling: { } } +export_statement_mangling: { + mangle = { }; + input: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } + expect: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From 41f8fcb46ff46465fc38390797d177c563896bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 13:27:18 +0000 Subject: [PATCH 104/105] fix crash: Import statements don't abort --- lib/compress.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compress.js b/lib/compress.js index bb77f6a6..bb4e73ac 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1203,6 +1203,7 @@ merge(Compressor.prototype, { var n = this.body.length; return n > 0 && aborts(this.body[n - 1]); }; + def(AST_Import, function(){ return null; }); def(AST_BlockStatement, block_aborts); def(AST_SwitchBranch, block_aborts); def(AST_If, function(){ From 747eb21a398779663548bea543f6df4e9d32f340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 12 Mar 2016 15:53:57 +0000 Subject: [PATCH 105/105] fix #1003 by removing AST_ObjectSymbol and using AST_ObjectKeyVal for the same effect --- lib/ast.js | 21 +++++---------------- lib/output.js | 25 ++++++++----------------- lib/parse.js | 8 +++++--- lib/propmangle.js | 8 -------- lib/transform.js | 4 ---- test/compress/harmony.js | 14 ++++++++++++++ 6 files changed, 32 insertions(+), 48 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 71459770..25905223 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -395,9 +395,9 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.properties.map(to_fun_args) }); - } else if (ex instanceof AST_ObjectSymbol) { + } else if (ex instanceof AST_ObjectKeyVal && ex.shorthand) { return new AST_SymbolFunarg({ - name: ex.symbol.name, + name: ex.key, start: ex.start, end: ex.end }); @@ -981,10 +981,11 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote shorthand", { $documentation: "A key: value object property", $propdoc: { - quote: "[string] the original quote character" + quote: "[string] the original quote character", + shorthand: "[boolean] whether this is a shorthand key:value pair, expressed as just the key." } }, AST_ObjectProperty); @@ -998,18 +999,6 @@ var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { } }, AST_ObjectProperty); -var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { - $propdoc: { - symbol: "[AST_SymbolRef] what symbol it is" - }, - $documentation: "A symbol in an object", - _walk: function (visitor) { - return visitor._visit(this, function(){ - this.symbol._walk(visitor); - }); - } -}, AST_ObjectProperty); - var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { $propdoc: { static: "[boolean] whether this is a static setter (classes only)" diff --git a/lib/output.js b/lib/output.js index 35b4816f..53a3a8d1 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1305,6 +1305,14 @@ function OutputStream(options) { DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; + var print_as_shorthand = self.shorthand && + self.value instanceof AST_Symbol && + self.key == self.value.print_to_string(); + + if (print_as_shorthand) { + output.print_name(key); + return; + } if (output.option("quote_keys")) { output.print_string(key + ""); } else if ((typeof key == "number" @@ -1363,23 +1371,6 @@ function OutputStream(options) { self.default.print(output) } }); - DEFPRINT(AST_ObjectSymbol, function(self, output){ - var name = self.mangled_key || self.symbol.name; - var def = self.symbol.definition(); - if (def && def.mangled_name) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.mangled_name); - } else if (!(def && def.mangled_name) && self.mangled_key) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.name); - } else { - output.print(name); - } - }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index 4f33f4b6..44d3cd59 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1518,14 +1518,16 @@ function parse($TEXT, options) { })); } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectSymbol({ + a.push(new AST_ObjectKeyVal({ start: start, end: start, - symbol: new AST_SymbolRef({ + key: name, + value: new AST_SymbolRef({ start: start, end: start, name: name - }) + }), + shorthand: true, })); } else { expect(":"); diff --git a/lib/propmangle.js b/lib/propmangle.js index 86da5de9..050f07d7 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -90,9 +90,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { add(node.key); } - else if (node instanceof AST_ObjectSymbol) { - add(node.symbol.name); - } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above add(node.key.name); @@ -117,11 +114,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { node.key = mangle(node.key); } - else if (node instanceof AST_ObjectSymbol) { - if (should_mangle(node.symbol.name)) { - node.mangled_key = mangle(node.symbol.name) - } - } else if (node instanceof AST_ObjectProperty) { // setter or getter node.key.name = mangle(node.key.name); diff --git a/lib/transform.js b/lib/transform.js index 5cb5c104..34663351 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -219,10 +219,6 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); - _(AST_ObjectSymbol, function(self, tw){ - self.symbol = self.symbol.transform(tw); - }); - _(AST_ObjectProperty, function(self, tw){ self.value = self.value.transform(tw); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index fec3a835..da3f9d4e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -54,6 +54,20 @@ computed_property_names: { expect_exact: 'obj({["x"+"x"]:6});' } +shorthand_properties: { + mangle = true; + input: (function() { + var prop = 1; + const value = {prop}; + return value; + })(); + expect: (function() { + var a = 1; + const b = {prop:a}; + return b; + })(); +} + typeof_arrow_functions: { options = { evaluate: true