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){});" +} +