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] 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() {