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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] => 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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. */ + } +} From 0d8dea9538c2305c3bacd507439b9f06abed2f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 11 Oct 2015 18:22:07 +0100 Subject: [PATCH 35/69] start concise methods --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 10 ++++++++++ test/compress/harmony.js | 16 ++++++++++++++++ 4 files changed, 33 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index d9afcff4..32d8486f 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,6 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); +var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { + $documentation: "An ES6 concise method inside an object or class" +}, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { $documentation: "A function definition" }, AST_Lambda); diff --git a/lib/output.js b/lib/output.js index f8929020..bdab47c6 100644 --- a/lib/output.js +++ b/lib/output.js @@ -832,6 +832,9 @@ function OutputStream(options) { } if (needs_parens) { output.print(")") } }); + DEFPRINT(AST_ConciseMethod, function(self, output){ + self._do_print(output, true /* do not print "function" */); + }); /* -----[ exits ]----- */ AST_Exit.DEFMETHOD("_do_print", function(output, kind){ diff --git a/lib/parse.js b/lib/parse.js index 95e44914..9fd4dcdf 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1446,6 +1446,16 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type == "name" && !is("punc", ":")) { + if (is("punc", "(")) { + a.push(new AST_ConciseMethod({ + start : start, + name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + })) + continue; + } if (name == "get") { a.push(new AST_ObjectGetter({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 54be70d4..718a547a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,22 @@ destructuring_arguments: { } } +concise_methods: { + input: { + x = { + foo(a, b) { + return x; + } + } + y = { + foo([{a}]) { + return a; + } + } + } + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" +} + number_literals: { input: { 0b1001; From da8c428a077df6c579f26de950dc9f1028f22bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 23 Oct 2015 18:59:07 +0100 Subject: [PATCH 36/69] Just making sure that concise methods are separated by commas. When classes come, they won't be necessary. --- test/compress/harmony.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 718a547a..19628be4 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -142,10 +142,11 @@ concise_methods: { y = { foo([{a}]) { return a; - } + }, + bar(){} } } - expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a}};" + expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } number_literals: { From 34213ea2f8c80dde85d31dffb2ef173dd6b7bc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:14:55 +0000 Subject: [PATCH 37/69] Create a new symbol for methods' names --- lib/ast.js | 4 ++++ lib/parse.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 32d8486f..8dadf21c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -991,6 +991,10 @@ var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { $documentation: "Symbol defining a function", }, AST_SymbolDeclaration); +var AST_SymbolMethod = DEFNODE("SymbolMethod", null, { + $documentation: "Symbol in an object defining a method", +}, AST_Symbol); + var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index 9fd4dcdf..b44cc468 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1449,7 +1449,7 @@ function parse($TEXT, options) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, - name : new AST_Symbol({ name: name }), // TODO what symbol is this really? + name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), end : prev() From c99eaae360c8baab973a7c9d7e14dbbb2ade8529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 22:15:21 +0000 Subject: [PATCH 38/69] Make concise methods work with propmangle --- lib/propmangle.js | 8 ++++++++ test/compress/harmony.js | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/propmangle.js b/lib/propmangle.js index ff782b57..86da5de9 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -107,6 +107,9 @@ function mangle_properties(ast, options) { addStrings(node.property); } } + else if (node instanceof AST_ConciseMethod) { + add(node.name.name); + } })); // step 2: transform the tree, renaming properties @@ -129,6 +132,11 @@ function mangle_properties(ast, options) { else if (node instanceof AST_Sub) { node.property = mangleStrings(node.property); } + else if (node instanceof AST_ConciseMethod) { + if (should_mangle(node.name.name)) { + node.name.name = mangle(node.name.name); + } + } // else if (node instanceof AST_String) { // if (should_mangle(node.value)) { // AST_Node.warn( diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 19628be4..2525d252 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -149,6 +149,26 @@ concise_methods: { expect_exact: "x={foo(a,b){return x}};y={foo([{a}]){return a},bar(){}};" } +concise_methods_and_mangle_props: { + mangle_props = { + regex: /_/ + }; + input: { + function x() { + obj = { + _foo() { return 1; } + } + } + } + expect: { + function x() { + obj = { + a() { return 1; } + } + } + } +} + number_literals: { input: { 0b1001; From 64e7a00399a1bcdc9679f97db885e0bd00ed957f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Mon, 26 Oct 2015 23:24:04 +0000 Subject: [PATCH 39/69] Accept keyword names as concise method names --- lib/parse.js | 2 +- test/compress/harmony.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index b44cc468..4ed32863 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1445,7 +1445,7 @@ function parse($TEXT, options) { var start = S.token; var type = start.type; var name = as_property_name(); - if (type == "name" && !is("punc", ":")) { + if (type != "string" && type != "num" && !is("punc", ":")) { if (is("punc", "(")) { a.push(new AST_ConciseMethod({ start : start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 2525d252..731976f3 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -169,6 +169,18 @@ concise_methods_and_mangle_props: { } } +concise_methods_and_keyword_names: { + input: { + x = { + catch() {}, + throw() {} + } + } + expect: { + x={catch(){},throw(){}}; + } +} + number_literals: { input: { 0b1001; From 5f7cb6939c1d9006f208ffe804fef64aade0ee23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:40:46 +0000 Subject: [PATCH 40/69] Starting ES6 classes --- lib/ast.js | 12 +++++ lib/output.js | 31 ++++++++++++ lib/parse.js | 106 ++++++++++++++++++++++++++++----------- test/compress/harmony.js | 19 +++++++ 4 files changed, 140 insertions(+), 28 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 8dadf21c..a80ea76c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -955,6 +955,14 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { $documentation: "An object getter property", }, AST_ObjectProperty); +var AST_Class = DEFNODE("Class", "name extends", { + $propdoc: { + name: "[AST_SymbolClassName?] optional class name.", + extends: "[AST_Node]? optional parent class", + }, + $documentation: "An ES6 class", +}, AST_Object); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -999,6 +1007,10 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); +var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { + $documentation: "Symbol naming a class's name. Lexically scoped to the class." +}, AST_SymbolDeclaration); + var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); diff --git a/lib/output.js b/lib/output.js index bdab47c6..e597184f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -502,6 +502,12 @@ function OutputStream(options) { return first_in_statement(output); }); + // Not a class, though. It can be alone in a statement although + // it extends from AST_Object. + PARENS(AST_Class, function() { + return false; + }); + PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; @@ -1188,6 +1194,31 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_Class, function(self, output){ + output.print("class"); + output.space(); + if (self.name) { + self.name.print(output); + output.space(); + } + if (self.extends) { + output.print("extends"); + output.space(); + self.extends.print(output); + output.space(); + } + if (self.properties.length > 0) output.with_block(function(){ + self.properties.forEach(function(prop, i){ + if (i) { + output.newline(); + } + output.indent(); + prop.print(output); + }); + output.newline(); + }); + else output.print("{}"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index 4ed32863..9af773d5 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -var KEYWORDS = 'break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; @@ -855,6 +855,9 @@ function parse($TEXT, options) { case "for": return for_(); + case "class": + return class_(); + case "function": return function_(AST_Defun); @@ -1374,6 +1377,13 @@ function parse($TEXT, options) { func.end = prev(); return subscripts(func, allow_calls); } + if (is("keyword", "class")) { + next(); + var cls = class_(); + cls.start = start; + cls.end = prev(); + return subscripts(cls, allow_calls); + } if (ATOMIC_START_TOKEN[S.token.type]) { return subscripts(as_atom_node(), allow_calls); } @@ -1446,32 +1456,9 @@ function parse($TEXT, options) { var type = start.type; var name = as_property_name(); if (type != "string" && type != "num" && !is("punc", ":")) { - if (is("punc", "(")) { - a.push(new AST_ConciseMethod({ - start : start, - name : new AST_SymbolMethod({ name: name }), - argnames : params_or_seq_().as_params(croak), - body : _function_body(true), - end : prev() - })) - continue; - } - if (name == "get") { - a.push(new AST_ObjectGetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); - continue; - } - if (name == "set") { - a.push(new AST_ObjectSetter({ - start : start, - key : as_atom_node(), - value : function_(AST_Accessor), - end : prev() - })); + var concise = concise_method_or_getset(name, start); + if (concise) { + a.push(concise); continue; } } @@ -1510,6 +1497,69 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); + function class_() { + var start, method, class_name, name, extends_, a = []; + + if (S.token.type == "name" && S.token.value != "extends") { + class_name = as_symbol(AST_SymbolClassName) + } + + if (S.token.value == "extends") { + next(); + extends_ = expression(true); + } + + expect("{"); + + if (is("punc", ";")) { next(); } // Leading semicolons are okay in class bodies. + while (!is("punc", "}")) { + start = S.token; + name = as_property_name(); + method = concise_method_or_getset(name, start); + if (!method) { croak(); } + a.push(method); + if (is("punc", ";")) { next(); } + } + + next(); + + return new AST_Class({ + start: start, + name: class_name, + extends: extends_, + properties: a, + end: prev(), + }); + } + + function concise_method_or_getset(name, start) { + if (is("punc", "(")) { + return new AST_ConciseMethod({ + start : start, + name : new AST_SymbolMethod({ name: name }), + argnames : params_or_seq_().as_params(croak), + body : _function_body(true), + end : prev() + }); + } + if (name == "get") { + return new AST_ObjectGetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + if (name == "set") { + return new AST_ObjectSetter({ + start : start, + key : as_atom_node(), + value : function_(AST_Accessor), + end : prev() + }); + } + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 731976f3..95e9bd54 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -181,6 +181,25 @@ concise_methods_and_keyword_names: { } } +classes: { + input: { + class SomeClass { + constructor() { + }; + foo() {}; + }; + class NoSemi { + constructor(...args) { + } + foo() {} + }; + class ChildClass extends SomeClass {}; + var asExpression = class AsExpression {}; + var nameless = class {}; + } + expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" +} + number_literals: { input: { 0b1001; From 9ffed2bea6ff4cf2e487781adaee2ee7944e2d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 27 Oct 2015 00:51:47 +0000 Subject: [PATCH 41/69] static properties --- lib/ast.js | 15 ++++++++++++--- lib/output.js | 16 +++++++++++++++- lib/parse.js | 9 +++++++++ test/compress/harmony.js | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index a80ea76c..9e2f3cee 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -473,7 +473,10 @@ var AST_Arrow = DEFNODE("Arrow", null, { $documentation: "An ES6 Arrow function ((a) => b)" }, AST_Lambda); -var AST_ConciseMethod = DEFNODE("ConciseMethod", null, { +var AST_ConciseMethod = DEFNODE("ConciseMethod", "static", { + $propdoc: { + static: "[boolean] whether this method is static (classes only)", + }, $documentation: "An ES6 concise method inside an object or class" }, AST_Lambda); @@ -947,11 +950,17 @@ var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { } }, AST_ObjectProperty); -var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { +var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static setter (classes only)" + }, $documentation: "An object setter property", }, AST_ObjectProperty); -var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { +var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { + $propdoc: { + static: "[boolean] whether this is a static getter (classes only)" + }, $documentation: "An object getter property", }, AST_ObjectProperty); diff --git a/lib/output.js b/lib/output.js index e597184f..e744788f 100644 --- a/lib/output.js +++ b/lib/output.js @@ -775,9 +775,11 @@ function OutputStream(options) { var self = this; if (!nokeyword) { output.print("function"); + if (self.name) { + output.space(); + } } if (self.name) { - output.space(); self.name.print(output); } output.with_parens(function(){ @@ -839,6 +841,10 @@ function OutputStream(options) { if (needs_parens) { output.print(")") } }); DEFPRINT(AST_ConciseMethod, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } self._do_print(output, true /* do not print "function" */); }); @@ -1238,12 +1244,20 @@ function OutputStream(options) { self.value.print(output); }); DEFPRINT(AST_ObjectSetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("set"); output.space(); self.key.print(output); self.value._do_print(output, true); }); DEFPRINT(AST_ObjectGetter, function(self, output){ + if (self.static) { + output.print("static"); + output.space(); + } output.print("get"); output.space(); self.key.print(output); diff --git a/lib/parse.js b/lib/parse.js index 9af773d5..5aee6017 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1533,9 +1533,16 @@ function parse($TEXT, options) { } function concise_method_or_getset(name, start) { + var is_static = false; + if (name === "static" && !is("punc", "(")) { + is_static = true; + name = S.token.value; + next(); + } if (is("punc", "(")) { return new AST_ConciseMethod({ start : start, + static : is_static, name : new AST_SymbolMethod({ name: name }), argnames : params_or_seq_().as_params(croak), body : _function_body(true), @@ -1545,6 +1552,7 @@ function parse($TEXT, options) { if (name == "get") { return new AST_ObjectGetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() @@ -1553,6 +1561,7 @@ function parse($TEXT, options) { if (name == "set") { return new AST_ObjectSetter({ start : start, + static: is_static, key : as_atom_node(), value : function_(AST_Accessor), end : prev() diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 95e9bd54..e1b3078a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -200,6 +200,20 @@ classes: { expect_exact: "class SomeClass{constructor(){}foo(){}}class NoSemi{constructor(...args){}foo(){}}class ChildClass extends SomeClass{}var asExpression=class AsExpression{};var nameless=class{};" } +class_statics: { + input: { + x = class { + static staticMethod() {} + static get foo() {} + static set bar() {} + static() { /* "static" can be a method name! */ } + get() { /* "get" can be a method name! */ } + set() { /* "set" can be a method name! */ } + } + } + expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" +} + number_literals: { input: { 0b1001; From 364d20f8fb8291525057451a491fe2924bd1f072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 20 Nov 2015 18:09:27 +0000 Subject: [PATCH 42/69] Add mangle = { ...mangleopts } option to tests. --- test/run-tests.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/run-tests.js b/test/run-tests.js index 1d0de8ca..6d7f7244 100755 --- a/test/run-tests.js +++ b/test/run-tests.js @@ -104,6 +104,9 @@ function run_compress_tests() { } var output = input.transform(cmp); output.figure_out_scope(); + if (test.mangle) { + output.mangle_names(test.mangle); + } output = make_code(output, false); if (expect != output) { log("!!! failed\n---INPUT---\n{input}\n---OUTPUT---\n{output}\n---EXPECTED---\n{expected}\n\n", { From bb6b3a773af0e2682689c738744226cf57d2340c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 20 Nov 2015 19:34:10 +0000 Subject: [PATCH 43/69] Make AST_Class inherit AST_Scope instead of AST_Object This is one of those days I'd love to use multiple inheritance. An AST_Class has lots of common with AST_Object, but unfortunately `instanceof AST_Scope` is used very, very much, and a class has its name inside its own special pocket scope. This compels me to make AST_Class inherit Scope instead. It looks like, although there is much in common with AST_Object, `instanceof AST_Object` seldom are made, perhaps because it is less often necessary to traverse an object than a scope. --- lib/ast.js | 18 ++++++++++++++++-- lib/output.js | 6 ------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 9e2f3cee..b78a85b2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -964,13 +964,27 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { $documentation: "An object getter property", }, AST_ObjectProperty); -var AST_Class = DEFNODE("Class", "name extends", { +var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { name: "[AST_SymbolClassName?] optional class name.", extends: "[AST_Node]? optional parent class", + properties: "[AST_ObjectProperty*] array of properties" }, $documentation: "An ES6 class", -}, AST_Object); + _walk: function(visitor) { + return visitor._visit(this, function(){ + if (this.name) { + this.name._walk(visitor); + } + if (this.extends) { + this.extends._walk(visitor); + } + this.properties.forEach(function(prop){ + prop._walk(visitor); + }); + }); + }, +}, AST_Scope); var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { diff --git a/lib/output.js b/lib/output.js index e744788f..d1ebdeee 100644 --- a/lib/output.js +++ b/lib/output.js @@ -502,12 +502,6 @@ function OutputStream(options) { return first_in_statement(output); }); - // Not a class, though. It can be alone in a statement although - // it extends from AST_Object. - PARENS(AST_Class, function() { - return false; - }); - PARENS([ AST_Unary, AST_Undefined ], function(output){ var p = output.parent(); return p instanceof AST_PropAccess && p.expression === this; From 69da8e53e022911b1f1b4b3785e694ce60c11c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 09:17:32 +0000 Subject: [PATCH 44/69] Separate class expressions from class declarations and their symbols like defuns --- lib/ast.js | 16 ++++++++++++++-- lib/parse.js | 14 +++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index b78a85b2..faae55fd 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -966,7 +966,7 @@ var AST_ObjectGetter = DEFNODE("ObjectGetter", "static", { var AST_Class = DEFNODE("Class", "name extends properties", { $propdoc: { - name: "[AST_SymbolClassName?] optional class name.", + name: "[AST_SymbolClass|AST_SymbolDefClass?] optional class name.", extends: "[AST_Node]? optional parent class", properties: "[AST_ObjectProperty*] array of properties" }, @@ -986,6 +986,14 @@ var AST_Class = DEFNODE("Class", "name extends properties", { }, }, AST_Scope); +var AST_DefClass = DEFNODE("DefClass", null, { + $documentation: "A class definition", +}, AST_Class); + +var AST_ClassExpression = DEFNODE("ClassExpression", null, { + $documentation: "A class expression." +}, AST_Class); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $propdoc: { name: "[string] name of this symbol", @@ -1030,7 +1038,11 @@ var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { $documentation: "Symbol naming a function expression", }, AST_SymbolDeclaration); -var AST_SymbolClassName = DEFNODE("SymbolClassName", null, { +var AST_SymbolDefClass = DEFNODE("SymbolDefClass", null, { + $documentation: "Symbol naming a class's name in a class declaration. Lexically scoped to its containing scope, and accessible within the class." +}, AST_SymbolDeclaration); + +var AST_SymbolClass = DEFNODE("SymbolClass", null, { $documentation: "Symbol naming a class's name. Lexically scoped to the class." }, AST_SymbolDeclaration); diff --git a/lib/parse.js b/lib/parse.js index 5aee6017..9341ad39 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -856,7 +856,7 @@ function parse($TEXT, options) { return for_(); case "class": - return class_(); + return class_(AST_DefClass); case "function": return function_(AST_Defun); @@ -1379,7 +1379,7 @@ function parse($TEXT, options) { } if (is("keyword", "class")) { next(); - var cls = class_(); + var cls = class_(AST_ClassExpression); cls.start = start; cls.end = prev(); return subscripts(cls, allow_calls); @@ -1497,11 +1497,15 @@ function parse($TEXT, options) { return new AST_Object({ properties: a }) }); - function class_() { + function class_(KindOfClass) { var start, method, class_name, name, extends_, a = []; if (S.token.type == "name" && S.token.value != "extends") { - class_name = as_symbol(AST_SymbolClassName) + class_name = as_symbol(KindOfClass === AST_DefClass ? AST_SymbolDefClass : AST_SymbolClass); + } + + if (KindOfClass === AST_DefClass && !class_name) { + croak(); } if (S.token.value == "extends") { @@ -1523,7 +1527,7 @@ function parse($TEXT, options) { next(); - return new AST_Class({ + return new KindOfClass({ start: start, name: class_name, extends: extends_, From 425613b0d2ab8dfa5c3f4cff5d9e1c5a26bbbca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 12:20:20 +0000 Subject: [PATCH 45/69] mangle class names --- lib/scope.js | 16 ++++++++++++++++ test/compress/harmony.js | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/scope.js b/lib/scope.js index caaba157..4abebddb 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -155,6 +155,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // later. (node.scope = defun.parent_scope).def_function(node); } + else if (node instanceof AST_SymbolClass) { + defun.def_variable(node); + } + else if (node instanceof AST_SymbolDefClass) { + // This deals with the name of the class being available + // inside the class. + (node.scope = defun.parent_scope).def_function(node); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { var def = defun.def_variable(node); @@ -171,6 +179,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // pass 2: find back references and eval var func = null; + var cls = null; var globals = self.globals = new Dictionary(); var tw = new TreeWalker(function(node, descend){ if (node instanceof AST_Lambda) { @@ -180,6 +189,13 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ func = prev_func; return true; } + if (node instanceof AST_Class) { + var prev_cls = cls; + cls = node; + descend(); + cls = prev_cls; + return true; + } if (node instanceof AST_SymbolRef) { var name = node.name; var sym = node.scope.find_variable(name); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e1b3078a..c959b5d1 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -214,6 +214,25 @@ class_statics: { expect_exact: "x=class{static staticMethod(){}static get foo(){}static set bar(){}static(){}get(){}set(){}};" } +class_name_can_be_mangled: { + mangle = { }; + input: { + function x() { + class Foo { + } + var class1 = Foo + var class2 = class Bar {} + } + } + expect: { + function x() { + class a { } + var b = a + var c = class a {} + } + } +} + number_literals: { input: { 0b1001; From e076abdbf24bc6acdfe34a11b607613016692e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 13:59:18 +0000 Subject: [PATCH 46/69] Mangle class names correctly --- lib/scope.js | 9 +++++++-- test/compress/harmony.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index 4abebddb..3ae51dd4 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -66,7 +66,11 @@ SymbolDef.prototype = { || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) || (options.keep_fnames && (this.orig[0] instanceof AST_SymbolLambda - || this.orig[0] instanceof AST_SymbolDefun)); + || this.orig[0] instanceof AST_SymbolDefun)) + || this.orig[0] instanceof AST_SymbolMethod + || (options.keep_classnames + && (this.orig[0] instanceof AST_SymbolClass + || this.orig[0] instanceof AST_SymbolDefClass)); }, mangle: function(options) { var cache = options.cache && options.cache.props; @@ -379,7 +383,8 @@ AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options){ sort : false, toplevel : false, screw_ie8 : false, - keep_fnames : false + keep_fnames : false, + keep_classnames : false }); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c959b5d1..1d18301d 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -233,6 +233,24 @@ class_name_can_be_mangled: { } } +class_name_can_be_preserved: { + mangle = { + keep_classnames: true + } + input: { + function x() { + (class Baz { }); + class Foo {}; + } + } + expect: { + function x() { + (class Baz { }); + class Foo {}; + } + } +} + number_literals: { input: { 0b1001; From a800356ad089c81a4bc25a90d58bc0071ebe7a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 21 Nov 2015 14:48:23 +0000 Subject: [PATCH 47/69] Implement new.target --- lib/ast.js | 4 ++++ lib/output.js | 3 +++ lib/parse.js | 8 ++++++++ test/compress/harmony.js | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/lib/ast.js b/lib/ast.js index faae55fd..c315c4a1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1003,6 +1003,10 @@ var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { $documentation: "Base class for all symbols", }); +var AST_NewTarget = DEFNODE("NewTarget", null, { + $documentation: "A reference to new.target" +}); + var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); diff --git a/lib/output.js b/lib/output.js index d1ebdeee..74d88d25 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1219,6 +1219,9 @@ function OutputStream(options) { }); else output.print("{}"); }); + DEFPRINT(AST_NewTarget, function(self, output) { + output.print("new.target"); + }); DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; diff --git a/lib/parse.js b/lib/parse.js index 9341ad39..8f03435f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1292,6 +1292,14 @@ function parse($TEXT, options) { var new_ = function(allow_calls) { var start = S.token; expect_token("operator", "new"); + if (is("punc", ".")) { + next(); + expect_token("name"); + return subscripts(new AST_NewTarget({ + start : start, + end : prev() + }), allow_calls); + } var newexp = expr_atom(false), args; if (is("punc", "(")) { next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 1d18301d..e53f9458 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -251,6 +251,14 @@ class_name_can_be_preserved: { } } +new_target: { + input: { + new.target; + new.target.name; + } + expect_exact: "new.target;new.target.name;" +} + number_literals: { input: { 0b1001; From f07ab4666fdc2358e342c2037f7ad94f97a205d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 48/69] Non-destructuring default parameters --- lib/ast.js | 17 ++++++++++++++++- lib/output.js | 18 +++++++++++++++--- test/compress/harmony.js | 8 ++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 8dadf21c..281912f7 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,6 +422,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { is_array: true, names: ex.elements.map(to_fun_args) }); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -983,8 +990,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index b0dc64ce..f1dc1ad1 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1225,9 +1225,21 @@ function OutputStream(options) { output.space(); self.value.print(output); }); - DEFPRINT(AST_Symbol, function(self, output){ - var def = self.definition(); - output.print_name(def ? def.mangled_name || def.name : self.name); + AST_Symbol.DEFMETHOD("_do_print", function(output){ + var def = this.definition(); + output.print_name(def ? def.mangled_name || def.name : this.name); + }); + DEFPRINT(AST_Symbol, function (self, output) { + self._do_print(output); + }); + DEFPRINT(AST_SymbolFunarg, function(self, output){ + self._do_print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }); DEFPRINT(AST_ObjectSymbol, function(self, output){ var name = self.mangled_key || self.symbol.name; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 731976f3..90836e5e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,14 @@ destructuring_arguments: { } } +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}" +} + concise_methods: { input: { x = { From cbcb3ac44e4fe5a56135aae358c3da6e849502d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 49/69] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 16 +++++++--------- lib/output.js | 6 ++++++ test/compress/harmony.js | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 281912f7..e58dcba2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -384,7 +384,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring var root = this; - return this.expressions.map(function to_fun_args(ex) { + return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { if (ex.properties.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -392,6 +392,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: false, + default: default_seen_above, names: ex.properties.map(to_fun_args) }); } else if (ex instanceof AST_ObjectSymbol) { @@ -408,6 +409,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, + default: default_seen_above, start: ex.start, end: ex.end }); @@ -420,15 +422,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: true, + default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -489,7 +487,7 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index f1dc1ad1..b2f34720 100644 --- a/lib/output.js +++ b/lib/output.js @@ -631,6 +631,12 @@ function OutputStream(options) { name.print(output); }) output.print(self.is_array ? "]" : "}"); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }) DEFPRINT(AST_Debugger, function(self, output){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 90836e5e..986dcd18 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -136,8 +136,9 @@ default_arguments: { input: { function x(a = 6) { } function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } } - expect_exact: "function x(a=6){}function x(a=6+5){}" + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } concise_methods: { From 59e999597ee84c889e78fbd9c29ae81c0dc21bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 50/69] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 20 +++++++++----------- lib/output.js | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index e58dcba2..e0f5fa75 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -973,10 +973,16 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." + init: "[AST_Node*/S] array of initializers for this declaration.", + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); } }, AST_Symbol); @@ -988,16 +994,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index b2f34720..a4b02725 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1238,7 +1238,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 8220dbbea0d2438931d17b1c8581f644606ad122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:04:42 +0000 Subject: [PATCH 51/69] Default values inside destructurings --- lib/parse.js | 24 +++++++++++++++++++++--- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 4ed32863..237977ee 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1249,8 +1249,12 @@ function parse($TEXT, options) { })); next(); } else if (is("name")) { - children.push(_make_symbol(sym_type)); - next(); + children.push(new (sym_type)({ + name : String(S.token.value), + start : S.token, + default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, + end : S.token + })); } else { children.push(expression()); } @@ -1485,7 +1489,21 @@ function parse($TEXT, options) { continue; } - if (!is("punc", ":")) { + if (is("operator", "=")) { + next(); + a.push(new AST_Assign({ + start: start, + // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. + left: new AST_SymbolRef({ + start: start, + end: start, + name: name + }), + operator: "=", + right: expression(false), + end: prev() + })); + } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectSymbol({ start: start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 986dcd18..a3910ac6 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -141,6 +141,16 @@ default_arguments: { expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + concise_methods: { input: { x = { From 084437bc6dc478c54c2bedad137489b59f78951d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 17:40:05 +0000 Subject: [PATCH 52/69] Non-destructuring default parameters --- lib/ast.js | 17 ++++++++++++++++- lib/output.js | 18 +++++++++++++++--- test/compress/harmony.js | 8 ++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0541dc89..343dd42d 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -422,6 +422,13 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { is_array: true, names: ex.elements.map(to_fun_args) }); + } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { + return new AST_SymbolFunarg({ + name: ex.left.name, + default: ex.right, + start: ex.start, + end: ex.end + }); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -1026,8 +1033,16 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { $documentation: "Symbol naming a function argument", + $propdoc: { + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); + } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 80f66e0e..4451d2e5 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1275,9 +1275,21 @@ function OutputStream(options) { output.space(); self.value.print(output); }); - DEFPRINT(AST_Symbol, function(self, output){ - var def = self.definition(); - output.print_name(def ? def.mangled_name || def.name : self.name); + AST_Symbol.DEFMETHOD("_do_print", function(output){ + var def = this.definition(); + output.print_name(def ? def.mangled_name || def.name : this.name); + }); + DEFPRINT(AST_Symbol, function (self, output) { + self._do_print(output); + }); + DEFPRINT(AST_SymbolFunarg, function(self, output){ + self._do_print(output); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }); DEFPRINT(AST_ObjectSymbol, function(self, output){ var name = self.mangled_key || self.symbol.name; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index e53f9458..a1f2aa9c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -132,6 +132,14 @@ destructuring_arguments: { } } +default_arguments: { + input: { + function x(a = 6) { } + function x(a = (6 + 5)) { } + } + expect_exact: "function x(a=6){}function x(a=6+5){}" +} + concise_methods: { input: { x = { From 5b553aafe29ccaebe35a6e3af65e190bb77cb856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 18:02:35 +0000 Subject: [PATCH 53/69] Destructuring parameters with defaults. `function x({ foo, bar } = {}) { }` --- lib/ast.js | 16 +++++++--------- lib/output.js | 6 ++++++ test/compress/harmony.js | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 343dd42d..78702e14 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -384,7 +384,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { as_params: function (croak) { // We don't want anything which doesn't belong in a destructuring var root = this; - return this.expressions.map(function to_fun_args(ex) { + return this.expressions.map(function to_fun_args(ex, _, __, default_seen_above) { if (ex instanceof AST_Object) { if (ex.properties.length == 0) croak("Invalid destructuring function parameter", ex.start.line, ex.start.col); @@ -392,6 +392,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: false, + default: default_seen_above, names: ex.properties.map(to_fun_args) }); } else if (ex instanceof AST_ObjectSymbol) { @@ -408,6 +409,7 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { } else if (ex instanceof AST_SymbolRef) { return new AST_SymbolFunarg({ name: ex.name, + default: default_seen_above, start: ex.start, end: ex.end }); @@ -420,15 +422,11 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { start: ex.start, end: ex.end, is_array: true, + default: default_seen_above, names: ex.elements.map(to_fun_args) }); - } else if (ex instanceof AST_Assign && ex.left instanceof AST_Symbol) { - return new AST_SymbolFunarg({ - name: ex.left.name, - default: ex.right, - start: ex.start, - end: ex.end - }); + } else if (ex instanceof AST_Assign) { + return to_fun_args(ex.left, undefined, undefined, ex.right); } else { croak("Invalid function parameter", ex.start.line, ex.start.col); } @@ -492,7 +490,7 @@ var AST_Defun = DEFNODE("Defun", null, { }, AST_Lambda); /* -----[ DESTRUCTURING ]----- */ -var AST_Destructuring = DEFNODE("Destructuring", "names is_array", { +var AST_Destructuring = DEFNODE("Destructuring", "names is_array default", { $documentation: "A destructuring of several names. Used in destructuring assignment and with destructuring function argument names", _walk: function(visitor) { return visitor._visit(this, function(){ diff --git a/lib/output.js b/lib/output.js index 4451d2e5..9ce64c40 100644 --- a/lib/output.js +++ b/lib/output.js @@ -639,6 +639,12 @@ function OutputStream(options) { name.print(output); }) output.print(self.is_array ? "]" : "}"); + if (self.default) { + output.space(); + output.print('='); + output.space(); + self.default.print(output) + } }) DEFPRINT(AST_Debugger, function(self, output){ diff --git a/test/compress/harmony.js b/test/compress/harmony.js index a1f2aa9c..69cf4caa 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -136,8 +136,9 @@ default_arguments: { input: { function x(a = 6) { } function x(a = (6 + 5)) { } + function x({ foo } = {}, [ bar ] = [ 1 ]) { } } - expect_exact: "function x(a=6){}function x(a=6+5){}" + expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } concise_methods: { From 7a8cffd631b6b76be3475c17e4a42889108ee114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:00:54 +0000 Subject: [PATCH 54/69] Move the idea of a symbol having a default value up the class chain. --- lib/ast.js | 20 +++++++++----------- lib/output.js | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 78702e14..358862b1 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -1016,10 +1016,16 @@ var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { $documentation: "The name of a property accessor (setter/getter function)" }, AST_Symbol); -var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { +var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init default", { $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", $propdoc: { - init: "[AST_Node*/S] array of initializers for this declaration." + init: "[AST_Node*/S] array of initializers for this declaration.", + default: "[AST_Expression] The default for this parameter. For example, `= 6`" + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + if (this.default) this.default._walk(visitor); + }); } }, AST_Symbol); @@ -1031,16 +1037,8 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, { $documentation: "A constant declaration" }, AST_SymbolDeclaration); -var AST_SymbolFunarg = DEFNODE("SymbolFunarg", "default", { +var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { $documentation: "Symbol naming a function argument", - $propdoc: { - default: "[AST_Expression] The default for this parameter. For example, `= 6`" - }, - _walk: function (visitor) { - return visitor._visit(this, function() { - if (this.default) this.default._walk(visitor); - }); - } }, AST_SymbolVar); var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { diff --git a/lib/output.js b/lib/output.js index 9ce64c40..c0b86b96 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1288,7 +1288,7 @@ function OutputStream(options) { DEFPRINT(AST_Symbol, function (self, output) { self._do_print(output); }); - DEFPRINT(AST_SymbolFunarg, function(self, output){ + DEFPRINT(AST_SymbolDeclaration, function(self, output){ self._do_print(output); if (self.default) { output.space(); From 028ff64e9aade190310aefd43fc6010113c42a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 22 Nov 2015 19:04:42 +0000 Subject: [PATCH 55/69] Default values inside destructurings --- lib/parse.js | 24 +++++++++++++++++++++--- test/compress/harmony.js | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 1a7bca94..8c8d19dd 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1255,8 +1255,12 @@ function parse($TEXT, options) { })); next(); } else if (is("name")) { - children.push(_make_symbol(sym_type)); - next(); + children.push(new (sym_type)({ + name : String(S.token.value), + start : S.token, + default: (next(), is("operator", "=")) ? (next(), expression(false)) : undefined, + end : S.token + })); } else { children.push(expression()); } @@ -1483,7 +1487,21 @@ function parse($TEXT, options) { continue; } - if (!is("punc", ":")) { + if (is("operator", "=")) { + next(); + a.push(new AST_Assign({ + start: start, + // Symbol class doesn't matter. This is only meant to carry the symbol name into .as_params() since this is not normally valid. + left: new AST_SymbolRef({ + start: start, + end: start, + name: name + }), + operator: "=", + right: expression(false), + end: prev() + })); + } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name a.push(new AST_ObjectSymbol({ start: start, diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 69cf4caa..c3f48b42 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -141,6 +141,16 @@ default_arguments: { expect_exact: "function x(a=6){}function x(a=6+5){}function x({foo}={},[bar]=[1]){}" } +default_values_in_destructurings: { + input: { + function x({a=(4), b}) {} + function x([b, c=(12)]) {} + var { x = (6), y } = x; + var [ x, y = (6) ] = x; + } + expect_exact: "function x({a=4,b}){}function x([b,c=12]){}var{x=6,y}=x;var[x,y=6]=x;" +} + concise_methods: { input: { x = { From 1f7523206217a0d552bd89d2d418e7b4a08e92a7 Mon Sep 17 00:00:00 2001 From: Fugiman Date: Mon, 18 Jan 2016 21:28:26 -0800 Subject: [PATCH 56/69] Fix template string parsing --- lib/parse.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 8c8d19dd..b90f6de4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1414,10 +1414,8 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - expect("}"); - if (is("punc", "`")) { - break; - } + if (!is("punc", "}")) + token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); continue; } segment += ch; From 5e78f20f1c6ea2ff340d8e7a4f022a576d8c297e Mon Sep 17 00:00:00 2001 From: Fugiman Date: Tue, 19 Jan 2016 02:04:17 -0800 Subject: [PATCH 57/69] Remove duplicate error message --- lib/parse.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index b90f6de4..072e7fb4 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1414,8 +1414,10 @@ function parse($TEXT, options) { tokenizer_S.next(); next(); segments.push(expression()); - if (!is("punc", "}")) - token_error(tokenizer_S.token, "Unexpected token " + tokenizer_S.token.type + " «" + tokenizer_S.token.value + "»" + ", expected punc «}»"); + if (!is("punc", "}")) { + // force error message + expect("}"); + } continue; } segment += ch; From c80ec625ec69656a57020d9007d7f53609502fe4 Mon Sep 17 00:00:00 2001 From: Richard van Velzen Date: Tue, 19 Jan 2016 19:21:53 +0100 Subject: [PATCH 58/69] Add test for bad template string parsing --- test/compress/issue-926.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/compress/issue-926.js diff --git a/test/compress/issue-926.js b/test/compress/issue-926.js new file mode 100644 index 00000000..e717efb0 --- /dev/null +++ b/test/compress/issue-926.js @@ -0,0 +1,9 @@ +template_strings: { + input: { + foo( + `${contents}`, + `${text}` + ); + } + expect_exact: "foo(`${contents}`,`${text}`);" +} From d7ec2ecc12ca24e5db94ec0fe634e6a41ff5dfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Tue, 9 Feb 2016 00:02:23 +0000 Subject: [PATCH 59/69] Fix #931: Create arrow functions in maybe_assign so that they can be used in assignments --- lib/parse.js | 21 ++++++++++++++++++++- test/compress/harmony.js | 11 +++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 072e7fb4..0a6072af 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1032,7 +1032,12 @@ function parse($TEXT, options) { var arrow_function = function(args) { expect_token("arrow", "=>"); - var argnames = args.as_params(croak); + var argnames; + if (typeof args.length === 'number') { + argnames = args; + } else { + argnames = args.as_params(croak); + } var body = is("punc", "{") ? _function_body(true) : @@ -1789,6 +1794,12 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; + if (start.value == "(" && peek().value == ")") { + next(); + next(); + return arrow_function([]); + } + var left = maybe_conditional(no_in); var val = S.token.value; @@ -1805,6 +1816,14 @@ function parse($TEXT, options) { } croak("Invalid assignment"); } + if (is("arrow")) { + left = new AST_SymbolFunarg({ + name: left.name, + start: left.start, + end: left.end, + }); + return arrow_function([left]) + } return left; }; diff --git a/test/compress/harmony.js b/test/compress/harmony.js index c3f48b42..622e00a0 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -36,6 +36,17 @@ regression_arrow_functions_and_hoist: { expect_exact: "a=>b;" } +regression_assign_arrow_functions: { + input: { + oninstall = e => false; + oninstall = () => false; + } + expect: { + oninstall=e=>false; + oninstall=()=>false; + } +} + computed_property_names: { input: { obj({ ["x" + "x"]: 6 }); From 0b303379c0cdc33a8c14c97ab29148d981b4887e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Javier=20Cravero?= Date: Thu, 4 Feb 2016 21:54:21 +0000 Subject: [PATCH 60/69] fix: don't fail if definition is undefined Running `uglifyjs --verbose --compress --mangle --screw-ie8 class.js` with `class.js`: ``` class Foo { bar() { } } ``` Fails with: ``` undefined:4041 return this.definition().unmangleable(options); TypeError: Cannot read property 'unmangleable' of undefined ... ``` --- lib/scope.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/scope.js b/lib/scope.js index ac58ad80..371afb2b 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -350,7 +350,8 @@ AST_Scope.DEFMETHOD("references", function(sym){ }); AST_Symbol.DEFMETHOD("unmangleable", function(options){ - return this.definition().unmangleable(options); + var def = this.definition(); + return def && def.unmangleable(options); }); // property accessors are not mangleable From 6780d0906c324102ec655d8e844754223876382a Mon Sep 17 00:00:00 2001 From: viclm Date: Tue, 8 Mar 2016 15:43:01 +0800 Subject: [PATCH 61/69] Fix eager parsing of arrow functions for non-punc tokens --- lib/parse.js | 2 +- test/compress/issue-1001.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 test/compress/issue-1001.js diff --git a/lib/parse.js b/lib/parse.js index 0a6072af..8e608e8a 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1794,7 +1794,7 @@ function parse($TEXT, options) { var maybe_assign = function(no_in) { var start = S.token; - if (start.value == "(" && peek().value == ")") { + if (start.type == "punc" && start.value == "(" && peek().value == ")") { next(); next(); return arrow_function([]); diff --git a/test/compress/issue-1001.js b/test/compress/issue-1001.js new file mode 100644 index 00000000..89adf81c --- /dev/null +++ b/test/compress/issue-1001.js @@ -0,0 +1,8 @@ +parenthesis_strings_in_parenthesis: { + input: { + ('('); + a(')'); + + } + expect_exact: '"(";a(")");' +} From 0465bd270d95e2fcf71c446438d3b9cfda527190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 29 Jan 2016 20:47:49 +0000 Subject: [PATCH 62/69] Starting out the import statement --- lib/ast.js | 7 +++++++ lib/output.js | 6 ++++++ lib/parse.js | 20 ++++++++++++++++++-- test/compress/harmony.js | 8 ++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 358862b1..0940647c 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,6 +702,13 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); +var AST_Import = DEFNODE("Import", "module_name", { + $documentation: "An `import` statement", + $propdoc: { + module_name: "[AST_String] String literal describing where this module came from", + } +}); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index c0b86b96..4dc53798 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1037,6 +1037,12 @@ function OutputStream(options) { DEFPRINT(AST_Const, function(self, output){ self._do_print(output, "const"); }); + DEFPRINT(AST_Import, function(self, output) { + output.print("import"); + output.space(); + self.module_name.print(output); + output.semicolon(); + }); function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); diff --git a/lib/parse.js b/lib/parse.js index 8e608e8a..4a549fb1 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,9 +44,9 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; var KEYWORDS_ATOM = 'false null true'; -var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield' +var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; var KEYWORDS_BEFORE_EXPRESSION = 'return new delete throw else case'; @@ -909,6 +909,9 @@ function parse($TEXT, options) { body : statement() }); + case "import": + return tmp = import_(), semicolon(), tmp; + default: unexpected(); } @@ -1607,6 +1610,19 @@ function parse($TEXT, options) { } } + function import_() { + return new AST_Import({ + start: prev(), + module_name: new AST_String({ + start : S.token, + value : S.token.value, + quote : S.token.quote, + end : S.token, + }), + end: next(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 622e00a0..4cb3921c 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -305,6 +305,14 @@ number_literals: { } } +import_statement: { + input: { + import "mod-name"; + import "module2"; + } + expect_exact: "import\"mod-name\";import\"module2\";" +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From d35a9e783920fd286cdbb1574e703f199a1415f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sun, 21 Feb 2016 17:06:09 +0000 Subject: [PATCH 63/69] Importing names from places --- lib/ast.js | 15 ++++++++++++++- lib/compress.js | 4 ++++ lib/output.js | 6 ++++++ lib/parse.js | 24 ++++++++++++++++++------ test/compress/harmony.js | 16 ++++++++++++++-- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 0940647c..3457c134 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "module_name", { +var AST_Import = DEFNODE("Import", "imported_name module_name", { $documentation: "An `import` statement", $propdoc: { + imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", module_name: "[AST_String] String literal describing where this module came from", + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + if (this.imported_name) { + this.imported_name._walk(visitor); + } + this.module_name._walk(visitor); + }); } }); @@ -1072,6 +1081,10 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { $documentation: "Symbol naming the exception in catch", }, AST_SymbolDeclaration); +var AST_SymbolImport = DEFNODE("SymbolImport", null, { + $documentation: "Symbol refering to an imported name", +}, AST_SymbolDeclaration); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/compress.js b/lib/compress.js index 52fbb74d..b1fbdf25 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1775,6 +1775,10 @@ merge(Compressor.prototype, { return self; }); + OPT(AST_Import, function(self, compressor) { + return self; + }); + OPT(AST_Function, function(self, compressor){ self = AST_Lambda.prototype.optimize.call(self, compressor); if (compressor.option("unused") && !compressor.option("keep_fnames")) { diff --git a/lib/output.js b/lib/output.js index 4dc53798..52747603 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1040,6 +1040,12 @@ function OutputStream(options) { DEFPRINT(AST_Import, function(self, output) { output.print("import"); output.space(); + if (self.imported_name) { + self.imported_name.print(output); + output.space(); + output.print("from") + output.space(); + } self.module_name.print(output); output.semicolon(); }); diff --git a/lib/parse.js b/lib/parse.js index 4a549fb1..4e94f589 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1611,15 +1611,27 @@ function parse($TEXT, options) { } function import_() { + var start = prev(); + var imported_name; + if (is("name")) { + imported_name = as_symbol(AST_SymbolImport); + expect_token("name", "from"); + } + var mod_str = S.token; + if (mod_str.type !== 'string') { + unexpected(); + } + next(); return new AST_Import({ - start: prev(), + start: start, + imported_name: imported_name, module_name: new AST_String({ - start : S.token, - value : S.token.value, - quote : S.token.quote, - end : S.token, + start: mod_str, + value: mod_str.value, + quote: mod_str.quote, + end: mod_str, }), - end: next(), + end: S.token, }); } diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 4cb3921c..dc1c2e4f 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -308,9 +308,21 @@ number_literals: { import_statement: { input: { import "mod-name"; - import "module2"; + import Foo from "bar"; + } + expect_exact: "import\"mod-name\";import Foo from\"bar\";" +} + +import_statement_mangling: { + mangle = { }; + input: { + import Foo from "foo"; + Foo(); + } + expect: { + import a from "foo"; + a(); } - expect_exact: "import\"mod-name\";import\"module2\";" } // Fabio: My patches accidentally caused a crash whenever From 59e1601fb8fbcf03a2e26ce7eea72afac86ee880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Fri, 26 Feb 2016 21:12:19 +0000 Subject: [PATCH 64/69] importing names in the modules, not just default imports --- lib/ast.js | 15 ++++++++++++++- lib/output.js | 31 +++++++++++++++++++++++++++++++ lib/parse.js | 40 ++++++++++++++++++++++++++++++++++++++++ test/compress/harmony.js | 5 ++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 3457c134..768bfc78 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -702,10 +702,19 @@ var AST_Const = DEFNODE("Const", null, { $documentation: "A `const` statement" }, AST_Definitions); -var AST_Import = DEFNODE("Import", "imported_name module_name", { +var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { + $documentation: "The part of the import statement that imports names from a module.", + $propdoc: { + foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", + name: "[AST_SymbolImport] The name as it becomes available to this module." + } +}) + +var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { $documentation: "An `import` statement", $propdoc: { imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.", + imported_names: "[AST_NameImport*] The names of non-default imported variables", module_name: "[AST_String] String literal describing where this module came from", }, _walk: function(visitor) { @@ -1085,6 +1094,10 @@ var AST_SymbolImport = DEFNODE("SymbolImport", null, { $documentation: "Symbol refering to an imported name", }, AST_SymbolDeclaration); +var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, { + $documentation: "A symbol imported from a module, but it is defined in the other module, and its real name is irrelevant for this module's purposes", +}, AST_Symbol); + var AST_Label = DEFNODE("Label", "references", { $documentation: "Symbol naming a label (declaration)", $propdoc: { diff --git a/lib/output.js b/lib/output.js index 52747603..a9c78bbf 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1042,6 +1042,25 @@ function OutputStream(options) { output.space(); if (self.imported_name) { self.imported_name.print(output); + } + if (self.imported_name && self.imported_names) { + output.print(","); + output.space(); + } + if (self.imported_names) { + output.print("{"); + self.imported_names.forEach(function(name_import, i) { + output.space(); + name_import.print(output); + if (i < self.imported_names.length - 1) { + output.print(","); + output.space(); + } + }); + output.space(); + output.print("}"); + } + if (self.imported_name || self.imported_names) { output.space(); output.print("from") output.space(); @@ -1050,6 +1069,18 @@ function OutputStream(options) { output.semicolon(); }); + DEFPRINT(AST_NameImport, function(self, output) { + if (self.foreign_name) { + self.foreign_name.print(output); + output.space(); + output.print("as"); + output.space(); + self.name.print(output); + } else { + self.name.print(output); + } + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index 4e94f589..fd51eda2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1613,8 +1613,28 @@ function parse($TEXT, options) { function import_() { var start = prev(); var imported_name; + var imported_names; if (is("name")) { imported_name = as_symbol(AST_SymbolImport); + } + + if (is("punc", ",")) { + next(); + } + + if (is("punc", "{")) { + next(); + imported_names = []; + while (!is("punc", "}")) { + imported_names.push(import_name()); + if (is("punc", ",")) { + next(); + } + } + next(); + } + + if (imported_names || imported_name) { expect_token("name", "from"); } var mod_str = S.token; @@ -1625,6 +1645,7 @@ function parse($TEXT, options) { return new AST_Import({ start: start, imported_name: imported_name, + imported_names: imported_names, module_name: new AST_String({ start: mod_str, value: mod_str.value, @@ -1635,6 +1656,25 @@ function parse($TEXT, options) { }); } + function import_name() { + var start = S.token; + var foreign_name; + var name; + + if (peek().value === "as" && peek().type === "name") { + foreign_name = as_symbol(AST_SymbolImportForeign); + next(); // The "as" word + } + name = as_symbol(AST_SymbolImport); + + return new AST_NameImport({ + start: start, + foreign_name: foreign_name, + name: name, + end: prev(), + }) + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index dc1c2e4f..59c6238a 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -309,8 +309,11 @@ import_statement: { input: { import "mod-name"; import Foo from "bar"; + import { Bar, Baz } from 'lel'; + import Bar, { Foo } from 'lel'; + import { Bar as kex, Baz as food } from 'lel'; } - expect_exact: "import\"mod-name\";import Foo from\"bar\";" + expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } import_statement_mangling: { From 86b5248837d3b4f2bfa14da6537e349b3306df26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:01:16 +0000 Subject: [PATCH 65/69] Mangling externally imported names by using aliasing --- lib/ast.js | 11 +++++++++++ lib/output.js | 8 ++++++-- lib/parse.js | 8 ++++++++ lib/scope.js | 9 +++++++++ test/compress/harmony.js | 10 ++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 768bfc78..6a814e35 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -707,6 +707,12 @@ var AST_NameImport = DEFNODE("NameImport", "foreign_name name", { $propdoc: { foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)", name: "[AST_SymbolImport] The name as it becomes available to this module." + }, + _walk: function (visitor) { + return visitor._visit(this, function() { + this.foreign_name._walk(visitor); + this.name._walk(visitor); + }); } }) @@ -722,6 +728,11 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { if (this.imported_name) { this.imported_name._walk(visitor); } + if (this.imported_names) { + this.imported_names.forEach(function (name_import) { + name_import._walk(visitor); + }); + } this.module_name._walk(visitor); }); } diff --git a/lib/output.js b/lib/output.js index a9c78bbf..e755ec04 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1070,8 +1070,12 @@ function OutputStream(options) { }); DEFPRINT(AST_NameImport, function(self, output) { - if (self.foreign_name) { - self.foreign_name.print(output); + var definition = self.name.definition(); + var names_are_different = + (definition && definition.mangled_name || self.name.name) !== + self.foreign_name.name; + if (names_are_different) { + output.print(self.foreign_name.name); output.space(); output.print("as"); output.space(); diff --git a/lib/parse.js b/lib/parse.js index fd51eda2..134b3f19 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1667,6 +1667,14 @@ function parse($TEXT, options) { } name = as_symbol(AST_SymbolImport); + if (foreign_name === undefined) { + foreign_name = new AST_SymbolImportForeign({ + name: name.name, + start: name.start, + end: name.end, + }); + } + return new AST_NameImport({ start: start, foreign_name: foreign_name, diff --git a/lib/scope.js b/lib/scope.js index 371afb2b..23f70ad1 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -171,6 +171,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ else if (node instanceof AST_SymbolClass) { defun.def_variable(node); } + else if (node instanceof AST_SymbolImport) { + scope.def_variable(node); + } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. @@ -302,6 +305,12 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ this.variables.set(symbol.name, def); def.object_destructuring_arg = symbol.object_destructuring_arg; def.global = !this.parent_scope; + if (symbol instanceof AST_SymbolImport) { + // Imports are not global + def.global = false; + // TODO The real fix comes with block scoping being first class in uglifyJS, + // enabling import definitions to behave like module-level let declarations + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 59c6238a..5ef1416e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -320,11 +320,21 @@ import_statement_mangling: { mangle = { }; input: { import Foo from "foo"; + import Bar, {Food} from "lel"; + import {What as Whatever} from "lel"; Foo(); + Bar(); + Food(); + Whatever(); } expect: { import a from "foo"; + import b, {Food as c} from "lel"; + import {What as d} from "lel"; a(); + b(); + c(); + d(); } } From ce84a706a3eab73589d0723bf8e03f8d4cbd776e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:24:18 +0000 Subject: [PATCH 66/69] Implement the export statement --- lib/ast.js | 9 +++++++++ lib/output.js | 15 +++++++++++++++ lib/parse.js | 35 ++++++++++++++++++++++++++++++++++- test/compress/harmony.js | 12 ++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/ast.js b/lib/ast.js index 6a814e35..e6867205 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -738,6 +738,15 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { } }); +var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default", { + $documentation: "An `export` statement", + $propdoc: { + exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition", + exported_value: "[AST_Node?] An exported value", + is_default: "[Boolean] Whether this is the default exported value of this module" + }, +}, AST_Statement); + var AST_VarDef = DEFNODE("VarDef", "name value", { $documentation: "A variable declaration; only appears in a AST_Definitions node", $propdoc: { diff --git a/lib/output.js b/lib/output.js index e755ec04..0dd5dcb9 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1085,6 +1085,21 @@ function OutputStream(options) { } }); + DEFPRINT(AST_Export, function(self, output) { + output.print("export"); + output.space(); + if (self.is_default) { + output.print("default"); + output.space(); + } + if (self.exported_value) { + self.exported_value.print(output); + } else if (self.exported_definition) { + self.exported_definition.print(output); + } + output.semicolon(); + }); + function parenthesize_for_noin(node, output, noin) { if (!noin) node.print(output); else try { diff --git a/lib/parse.js b/lib/parse.js index 134b3f19..5463576d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -44,7 +44,7 @@ "use strict"; -var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import'; +var KEYWORDS = 'break case catch class const continue debugger default delete do else extends finally for function if in instanceof new return switch throw try typeof var let void while with import export'; var KEYWORDS_ATOM = 'false null true'; var RESERVED_WORDS = 'abstract boolean byte char double enum export final float goto implements int interface long native package private protected public short static super synchronized this throws transient volatile yield' + " " + KEYWORDS_ATOM + " " + KEYWORDS; @@ -912,6 +912,9 @@ function parse($TEXT, options) { case "import": return tmp = import_(), semicolon(), tmp; + case "export": + return tmp = export_(), semicolon(), tmp; + default: unexpected(); } @@ -1683,6 +1686,36 @@ function parse($TEXT, options) { }) } + function export_() { + var start = S.token; + var is_default; + var exported_value; + var exported_definition; + + if (is("keyword", "default")) { + is_default = true; + next(); + } + + var is_definition = + is("keyword", "var") || is("keyword", "let") || is("keyword", "const") || + is("keyword", "class") || is("keyword", "function"); + + if (is_definition) { + exported_definition = statement(); + } else { + exported_value = expression(); + } + + return new AST_Export({ + start: start, + is_default: is_default, + exported_value: exported_value, + exported_definition: exported_definition, + end: prev(), + }); + } + function as_property_name() { var tmp = S.token; next(); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 5ef1416e..d27d903b 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -316,6 +316,18 @@ import_statement: { expect_exact: "import\"mod-name\";import Foo from\"bar\";import{Bar,Baz}from\"lel\";import Bar,{Foo}from\"lel\";import{Bar as kex,Baz as food}from\"lel\";" } +export_statement: { + input: { + export default 1; + export var foo = 4; + export let foo = 6; + export const foo = 6; + export function foo() {}; + export class foo { }; + } + expect_exact: "export default 1;export var foo=4;export let foo=6;export const foo=6;export function foo(){};export class foo{};" +} + import_statement_mangling: { mangle = { }; input: { From 0bc4f6edb4ccfa04b124e244268bd95a01d9f642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 12:40:57 +0000 Subject: [PATCH 67/69] Don't mangle exported symbols --- lib/ast.js | 10 ++++++++++ lib/scope.js | 32 ++++++++++++++++++++++---------- test/compress/harmony.js | 16 ++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index e6867205..160b1cc2 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -745,6 +745,16 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul exported_value: "[AST_Node?] An exported value", is_default: "[Boolean] Whether this is the default exported value of this module" }, + _walk: function (visitor) { + visitor._visit(this, function () { + if (this.exported_definition) { + this.exported_definition._walk(visitor); + } + if (this.exported_value) { + this.exported_value._walk(visitor); + } + }); + } }, AST_Statement); var AST_VarDef = DEFNODE("VarDef", "name value", { diff --git a/lib/scope.js b/lib/scope.js index 23f70ad1..f9241f2d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -49,6 +49,7 @@ function SymbolDef(scope, index, orig) { this.scope = scope; this.references = []; this.global = false; + this.export = false; this.mangled_name = null; this.object_destructuring_arg = false; this.undeclared = false; @@ -61,6 +62,7 @@ SymbolDef.prototype = { if (!options) options = {}; return (this.global && !options.toplevel) + || this.export || this.object_destructuring_arg || this.undeclared || (!options.eval && (this.scope.uses_eval || this.scope.uses_with)) @@ -102,6 +104,7 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ var defun = null; var nesting = 0; var in_destructuring = null; + var in_export; var tw = new TreeWalker(function(node, descend){ if (options.screw_ie8 && node instanceof AST_Catch) { var save_scope = scope; @@ -131,6 +134,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ labels = save_labels; return true; // don't descend again in TreeWalker } + if (node instanceof AST_Export) { + in_export = true; + descend(); + in_export = false; + } if (node instanceof AST_LabeledStatement) { var l = node.label; if (labels.has(l.name)) { @@ -151,14 +159,14 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ } if (node instanceof AST_SymbolFunarg) { node.object_destructuring_arg = !!in_destructuring; - defun.def_variable(node); + defun.def_variable(node, in_export); } if (node instanceof AST_Label) { node.thedef = node; node.references = []; } if (node instanceof AST_SymbolLambda) { - defun.def_function(node); + defun.def_function(node, in_export); } else if (node instanceof AST_SymbolDefun) { // Careful here, the scope where this should be defined is @@ -166,22 +174,22 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options){ // scope when we encounter the AST_Defun node (which is // instanceof AST_Scope) but we get to the symbol a bit // later. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolClass) { - defun.def_variable(node); + defun.def_variable(node, in_export); } else if (node instanceof AST_SymbolImport) { - scope.def_variable(node); + scope.def_variable(node, in_export); } else if (node instanceof AST_SymbolDefClass) { // This deals with the name of the class being available // inside the class. - (node.scope = defun.parent_scope).def_function(node); + (node.scope = defun.parent_scope).def_function(node, in_export); } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { - var def = defun.def_variable(node); + var def = defun.def_variable(node, in_export); def.constant = node instanceof AST_SymbolConst; def.destructuring = in_destructuring; def.init = tw.parent().value; @@ -294,11 +302,11 @@ AST_Scope.DEFMETHOD("find_variable", function(name){ || (this.parent_scope && this.parent_scope.find_variable(name)); }); -AST_Scope.DEFMETHOD("def_function", function(symbol){ - this.functions.set(symbol.name, this.def_variable(symbol)); +AST_Scope.DEFMETHOD("def_function", function(symbol, in_export){ + this.functions.set(symbol.name, this.def_variable(symbol, in_export)); }); -AST_Scope.DEFMETHOD("def_variable", function(symbol){ +AST_Scope.DEFMETHOD("def_variable", function(symbol, in_export){ var def; if (!this.variables.has(symbol.name)) { def = new SymbolDef(this, this.variables.size(), symbol); @@ -311,6 +319,10 @@ AST_Scope.DEFMETHOD("def_variable", function(symbol){ // TODO The real fix comes with block scoping being first class in uglifyJS, // enabling import definitions to behave like module-level let declarations } + if (!this.parent_scope && in_export) { + def.global = false; + def.export = true; + } } else { def = this.variables.get(symbol.name); def.orig.push(symbol); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index d27d903b..fec3a835 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -350,6 +350,22 @@ import_statement_mangling: { } } +export_statement_mangling: { + mangle = { }; + input: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } + expect: { + export var foo = 6; + export function bar() { } + export class Baz { } + bar(foo, Baz) + } +} + // Fabio: My patches accidentally caused a crash whenever // there's an extraneous set of parens around an object. regression_cannot_destructure: { From accca2445fcbf87b21702f34e378233dfaf195ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 27 Feb 2016 13:27:18 +0000 Subject: [PATCH 68/69] fix crash: Import statements don't abort --- lib/compress.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compress.js b/lib/compress.js index b1fbdf25..f96fb04a 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1025,6 +1025,7 @@ merge(Compressor.prototype, { var n = this.body.length; return n > 0 && aborts(this.body[n - 1]); }; + def(AST_Import, function(){ return null; }); def(AST_BlockStatement, block_aborts); def(AST_SwitchBranch, block_aborts); def(AST_If, function(){ From 6d2f77c18023833256d9059c1fbb36b5522b4131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Santos?= Date: Sat, 12 Mar 2016 15:53:57 +0000 Subject: [PATCH 69/69] fix #1003 by removing AST_ObjectSymbol and using AST_ObjectKeyVal for the same effect --- lib/ast.js | 21 +++++---------------- lib/output.js | 25 ++++++++----------------- lib/parse.js | 8 +++++--- lib/propmangle.js | 8 -------- lib/transform.js | 4 ---- test/compress/harmony.js | 14 ++++++++++++++ 6 files changed, 32 insertions(+), 48 deletions(-) diff --git a/lib/ast.js b/lib/ast.js index 160b1cc2..fc03a2c0 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -395,9 +395,9 @@ var AST_ArrowParametersOrSeq = DEFNODE("ArrowParametersOrSeq", "expressions", { default: default_seen_above, names: ex.properties.map(to_fun_args) }); - } else if (ex instanceof AST_ObjectSymbol) { + } else if (ex instanceof AST_ObjectKeyVal && ex.shorthand) { return new AST_SymbolFunarg({ - name: ex.symbol.name, + name: ex.key, start: ex.start, end: ex.end }); @@ -981,10 +981,11 @@ var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { } }); -var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote", { +var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", "quote shorthand", { $documentation: "A key: value object property", $propdoc: { - quote: "[string] the original quote character" + quote: "[string] the original quote character", + shorthand: "[boolean] whether this is a shorthand key:value pair, expressed as just the key." } }, AST_ObjectProperty); @@ -998,18 +999,6 @@ var AST_ObjectComputedKeyVal = DEFNODE("ObjectComputedKeyVal", null, { } }, AST_ObjectProperty); -var AST_ObjectSymbol = DEFNODE("ObjectSymbol", "symbol", { - $propdoc: { - symbol: "[AST_SymbolRef] what symbol it is" - }, - $documentation: "A symbol in an object", - _walk: function (visitor) { - return visitor._visit(this, function(){ - this.symbol._walk(visitor); - }); - } -}, AST_ObjectProperty); - var AST_ObjectSetter = DEFNODE("ObjectSetter", "static", { $propdoc: { static: "[boolean] whether this is a static setter (classes only)" diff --git a/lib/output.js b/lib/output.js index 0dd5dcb9..cbd13893 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1301,6 +1301,14 @@ function OutputStream(options) { DEFPRINT(AST_ObjectKeyVal, function(self, output){ var key = self.key; var quote = self.quote; + var print_as_shorthand = self.shorthand && + self.value instanceof AST_Symbol && + self.key == self.value.print_to_string(); + + if (print_as_shorthand) { + output.print_name(key); + return; + } if (output.option("quote_keys")) { output.print_string(key + ""); } else if ((typeof key == "number" @@ -1359,23 +1367,6 @@ function OutputStream(options) { self.default.print(output) } }); - DEFPRINT(AST_ObjectSymbol, function(self, output){ - var name = self.mangled_key || self.symbol.name; - var def = self.symbol.definition(); - if (def && def.mangled_name) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.mangled_name); - } else if (!(def && def.mangled_name) && self.mangled_key) { - output.print(name); - output.print(':'); - output.space(); - output.print(def.name); - } else { - output.print(name); - } - }); DEFPRINT(AST_Undefined, function(self, output){ output.print("void 0"); }); diff --git a/lib/parse.js b/lib/parse.js index 5463576d..6bc79215 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1514,14 +1514,16 @@ function parse($TEXT, options) { })); } else if (!is("punc", ":")) { // It's one of those object destructurings, the value is its own name - a.push(new AST_ObjectSymbol({ + a.push(new AST_ObjectKeyVal({ start: start, end: start, - symbol: new AST_SymbolRef({ + key: name, + value: new AST_SymbolRef({ start: start, end: start, name: name - }) + }), + shorthand: true, })); } else { expect(":"); diff --git a/lib/propmangle.js b/lib/propmangle.js index 86da5de9..050f07d7 100644 --- a/lib/propmangle.js +++ b/lib/propmangle.js @@ -90,9 +90,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { add(node.key); } - else if (node instanceof AST_ObjectSymbol) { - add(node.symbol.name); - } else if (node instanceof AST_ObjectProperty) { // setter or getter, since KeyVal is handled above add(node.key.name); @@ -117,11 +114,6 @@ function mangle_properties(ast, options) { if (node instanceof AST_ObjectKeyVal) { node.key = mangle(node.key); } - else if (node instanceof AST_ObjectSymbol) { - if (should_mangle(node.symbol.name)) { - node.mangled_key = mangle(node.symbol.name) - } - } else if (node instanceof AST_ObjectProperty) { // setter or getter node.key.name = mangle(node.key.name); diff --git a/lib/transform.js b/lib/transform.js index 2cea8705..dc3a068f 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -219,10 +219,6 @@ TreeTransformer.prototype = new TreeWalker; self.properties = do_list(self.properties, tw); }); - _(AST_ObjectSymbol, function(self, tw){ - self.symbol = self.symbol.transform(tw); - }); - _(AST_ObjectProperty, function(self, tw){ self.value = self.value.transform(tw); }); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index fec3a835..da3f9d4e 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -54,6 +54,20 @@ computed_property_names: { expect_exact: 'obj({["x"+"x"]:6});' } +shorthand_properties: { + mangle = true; + input: (function() { + var prop = 1; + const value = {prop}; + return value; + })(); + expect: (function() { + var a = 1; + const b = {prop:a}; + return b; + })(); +} + typeof_arrow_functions: { options = { evaluate: true