From 32f76f7ff89592e2dcb8cfa66d3ee1f95ae78d12 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 01/34] Starting destructuring. --- lib/ast.js | 90 +++++++++++++++++++++++++++++++++++++++- lib/compress.js | 34 ++++++++++----- lib/output.js | 11 +++++ lib/parse.js | 102 ++++++++++++++++++++++++++++++++------------- lib/scope.js | 14 +++++++ test/parser.js | 103 ++++++++++++++++++++++++++++++++++++++++++++++ test/run-tests.js | 4 ++ 7 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 test/parser.js diff --git a/lib/ast.js b/lib/ast.js index 2e539cff..b8562233 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 401a1c75..9a7ccfc3 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -231,6 +231,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 }); }); } @@ -1089,17 +1090,26 @@ merge(Compressor.prototype, { if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { if (compressor.option("unsafe") && !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; } } } @@ -1263,9 +1273,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(); @@ -1785,6 +1796,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 1d67b1b9..fcaa364b 100644 --- a/lib/output.js +++ b/lib/output.js @@ -598,6 +598,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 e65c4faa..4e16171a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -644,6 +644,7 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, + in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -957,35 +958,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")) { @@ -1224,6 +1248,7 @@ function parse($TEXT, options) { }); var object_ = embed_tokens(function() { + var start = S.token; expect("{"); var first = true, a = []; while (!is("punc", "}")) { @@ -1254,14 +1279,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 6c19c19a..a251f55b 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 @@ -94,6 +96,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var defun = null; 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; @@ -104,6 +107,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; @@ -127,6 +136,10 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ if (node instanceof AST_Symbol) { node.scope = scope; } + 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); } @@ -250,6 +263,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 215f6af8..92872f92 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -23,6 +23,10 @@ run_ast_conversion_tests({ iterations: 1000 }); +var run_parser_tests = require('./parser.js'); + +run_parser_tests(); + /* -----[ utils ]----- */ function tmpl() { From f7460166dd5f12a4966c6d55c6515f6d1dab4cf6 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 02/34] remove trace statement --- lib/ast.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index b8562233..16cfc387 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 4644becb9b2e272dcecf506c2911246c0298a3bb 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 03/34] 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 9a7ccfc3..bfdb1952 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -230,9 +230,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 96b89e34a3c404749b9b58efa78b490989aaba3e 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 04/34] 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 bfdb1952..9e94a6b7 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1280,7 +1280,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 ad344c5be31d9c5df1ae906d7bb49b430fffed88 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 05/34] 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 9e94a6b7..3788ab7d 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1802,7 +1802,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 9d7d365c2bfbd1e75197607d3a5ee97cc7fa6c6b 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 06/34] 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 16cfc387..7caccfd3 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -262,6 +262,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 fcaa364b..28c1facd 100644 --- a/lib/output.js +++ b/lib/output.js @@ -719,7 +719,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 4e16171a..7b175a7e 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; @@ -921,11 +921,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); @@ -945,6 +951,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 fa5c4f2d036abad60538b58ad22e5f863bcc5667 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 07/34] Adding arrow functions --- lib/ast.js | 2 +- lib/compress.js | 2 ++ lib/output.js | 28 ++++++++++++++++++++ lib/parse.js | 56 ++++++++++++++++++++++++++++++++++++++++ lib/transform.js | 6 ++++- test/compress/harmony.js | 37 ++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 test/compress/harmony.js diff --git a/lib/ast.js b/lib/ast.js index 7caccfd3..5c78a99f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -142,7 +142,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 3788ab7d..89fdaec5 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1019,6 +1019,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; }); @@ -1225,6 +1226,7 @@ merge(Compressor.prototype, { 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 28c1facd..80235ca9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -763,6 +763,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 7b175a7e..96d3b4c3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -510,6 +510,16 @@ function tokenizer($TEXT, filename, html5_comments) { 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)) @@ -559,6 +569,7 @@ function tokenizer($TEXT, filename, html5_comments) { 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()); @@ -975,6 +986,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 @@ -1486,6 +1532,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)) { @@ -1500,6 +1553,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 c3c34f58..41c82c99 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 a68953c491fdabc9476fdc8d4db50be87ccf5741 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 08/34] => 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 5c78a99f..0569133e 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 96d3b4c3..913da4bc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -989,35 +989,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 }); }; @@ -1030,7 +1012,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, @@ -1060,14 +1042,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; @@ -1263,16 +1249,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(); } @@ -1311,68 +1298,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() { @@ -1530,16 +1550,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(); @@ -1553,15 +1570,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 ddd30eeaaad77662edb7c0de5e500379e81e8095 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 09/34] 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 9863f0efa3e873a99cfa5b627c67e96761ee38de 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 10/34] 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 0569133e..2622cf18 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 89fdaec5..6b480390 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1107,6 +1107,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 0324d896..de54f909 100644 --- a/lib/output.js +++ b/lib/output.js @@ -607,6 +607,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 48bc70bb..ba87acf7 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -526,9 +526,16 @@ function tokenizer($TEXT, filename, html5_comments) { 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() { @@ -1034,7 +1041,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 @@ -1475,13 +1491,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 e80ed38772db814040e2cc7c2fe6840a82939b7b 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 11/34] 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 7caccfd3..078f748e 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -940,6 +940,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 28c1facd..275cf9b6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1151,6 +1151,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 7b175a7e..269bb235 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1361,7 +1361,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 @@ -1481,6 +1483,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 a251f55b..55d7c4e9 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -472,6 +472,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 6f864402d3a026b23838c624caf6d1d2db3d1f6d 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 12/34] Parse binary number literals --- lib/parse.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/parse.js b/lib/parse.js index ba87acf7..0b3b45cd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -59,6 +59,7 @@ 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([ @@ -182,6 +183,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_BIN_NUMBER.test(num)) { + return parseInt(num.substr(2), 2); } else if (RE_DEC_NUMBER.test(num)) { return parseFloat(num); } From dcce4e5c6625e2ebb14d12801ce934890e47081f 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 13/34] 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 6b480390..7b07667e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -738,6 +738,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"); @@ -756,7 +759,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 4c12cccff9ed22ce924cbf0bad2424fbd89af98e 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 14/34] 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 6b480390..f6fe0f48 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1831,6 +1831,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 a8f8aa518b3c61e5dec847fb6d5d262bc8c595a8 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 15/34] 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 0b3b45cd..f3a585d1 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 02d2b299..82cac95f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -97,3 +97,18 @@ destructuring_arguments: { } } +binary_literals: { + input: { + 0b1001; + 0B1001; + 0o11; + 0O11; + } + + expect: { + 9; + 9; + 9; + 9; + } +} From 36420183fd98107db055038bcf968335d947c1b7 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 16/34] 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 82cac95f..15130424 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -97,7 +97,7 @@ destructuring_arguments: { } } -binary_literals: { +number_literals: { input: { 0b1001; 0B1001; From ceebc466b9f5fb9da3c76a7efdfa6f5847e12312 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 17/34] 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 41c82c99..7858759a 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 824ecfb8a278858cbc4b430ef91f23f498ca7109 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 18/34] 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 c050aee1..fe56bed5 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 c44c2d6c21b9d0e884ca47aac294ee23d883ecc6 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 19/34] 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 fe56bed5..2c73b6d3 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 cbd1caee..51fd18dc 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1056,6 +1056,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)) { @@ -1142,6 +1143,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, @@ -1262,6 +1264,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; }); @@ -1702,13 +1705,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 2351dfc1..a0717c1f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1180,13 +1180,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(); @@ -1194,6 +1205,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 c1cf5c3c..9bee9a2b 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 025d34bfa216eaa09819de58d174b6892bc7af29 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 20/34] 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 a0717c1f..8f57bfca 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -753,7 +753,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; @@ -1182,17 +1182,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() }) @@ -1205,12 +1206,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 e99bc914ca4159828dc139b0be02f4e8f97e827c 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 21/34] Do not mangle a name if it is in a destructuring vardef. --- lib/scope.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 55d7c4e9..068daa60 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -96,7 +96,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var scope = self.parent_scope = null; var defun = null; 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; @@ -108,9 +108,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) { @@ -137,7 +137,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ node.scope = scope; } 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) { @@ -155,6 +155,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); def.constant = node instanceof AST_SymbolConst; + def.destructuring = in_destructuring; def.init = tw.parent().value; } else if (node instanceof AST_SymbolCatch) { @@ -412,7 +413,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 d4f17f29aec6ae9ddcc1b958f3fa4e02af606973 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 22/34] 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 8f57bfca..8ed7163a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1187,7 +1187,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 079aaa0d4858cb1ddbd8af9e49e50c63d2ced870 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 23/34] 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 2c73b6d3..7e7503ee 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 8ed7163a..fd870bbb 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1221,6 +1221,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 dc5db9b6ca485647cff2e1821de5bf560ec83cc8 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 24/34] 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 e2f7f060..080b3da8 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1179,6 +1179,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 fd870bbb..3d3eb664 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1410,9 +1410,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 7ee8f3512eef5eb3fe7d2f678d6c110c3fc810f2 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 25/34] 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 080b3da8..505f94b5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1180,14 +1180,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 7858759a..266e686f 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 adee5023c02ee929b68aa8a1ff3a220d67947831 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 26/34] 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 505f94b5..5d8bef10 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1187,6 +1187,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 e1cb1a0e3ca2312fd403d10fee5960691287a436 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 27/34] 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 7e7503ee..13260582 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 5d8bef10..c6cb254e 100644 --- a/lib/output.js +++ b/lib/output.js @@ -776,6 +776,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 3d3eb664..00ba3f55 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")); @@ -595,6 +595,9 @@ function tokenizer($TEXT, filename, html5_comments) { parse_error("Unexpected character '" + ch + "'"); }; + next_token.next = next; + next_token.peek = peek; + next_token.context = function(nc) { if (nc) S = nc; return S; @@ -805,6 +808,7 @@ function parse($TEXT, options) { }); case "[": case "(": + case "`": return simple_statement(); case ";": next(); @@ -1336,6 +1340,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(); } @@ -1352,6 +1358,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 242c61be9485ce3ea1bc0752ff80c38468dc5ce9 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 28/34] 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 13260582..818dde28 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 c6cb254e..2b40fdfc 100644 --- a/lib/output.js +++ b/lib/output.js @@ -776,6 +776,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 00ba3f55..759ce2a3 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1717,6 +1717,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 2fac2bbfe44c251e13316f43450900a828960d1f 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 29/34] 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 3d3eb664..35096bc6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -672,7 +672,6 @@ function parse($TEXT, options) { prev : null, peeked : null, in_function : 0, - in_parameters : false, in_directives : true, in_loop : 0, labels : [] @@ -1044,7 +1043,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", "...")) { @@ -1058,7 +1056,6 @@ function parse($TEXT, options) { a.push(expression(false)); } } - S.in_parameters = false; var end = S.token next(); return new AST_ArrowParametersOrSeq({ @@ -1375,97 +1372,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 2fcfd70f..49b06354 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -122,3 +122,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 dde9e293df99cb417c00a60290598a960f4992cc 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 30/34] 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 818dde28..3eba3a71 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 2b40fdfc..1172bd58 100644 --- a/lib/output.js +++ b/lib/output.js @@ -994,6 +994,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 974734b0..05384812 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; @@ -879,6 +879,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; @@ -949,13 +952,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) { @@ -1253,6 +1258,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 3d7f73114d52acbba9f482bdd289986253ba2ec0 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 31/34] 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 b31918bbf0e58d89cd1d4321d50c46a6ed5464f7 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 32/34] 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 07126e7c..d9afcff4 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 7dd43e69..c990712e 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -952,6 +952,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 03cda230..5aeed667 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1203,6 +1203,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 5ae2a1db..c0065217 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1465,6 +1465,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 @@ -1495,6 +1504,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 76ed083e47f9b4c4a7c876f38c333cbff3e1f981 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 33/34] 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 2cce61c564936f17c1ae174f3f2920e6a0e66ae8 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 34/34] 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 c0065217..95e44914 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. */ + } +}