Starting destructuring.

This commit is contained in:
Fábio Santos
2015-01-15 03:03:38 +00:00
committed by Richard van Velzen
parent 252fc65558
commit 32f76f7ff8
7 changed files with 317 additions and 41 deletions

View File

@@ -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);

View File

@@ -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()

View File

@@ -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();

View File

@@ -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 });

View File

@@ -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);

103
test/parser.js Normal file
View File

@@ -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();
}

View File

@@ -23,6 +23,10 @@ run_ast_conversion_tests({
iterations: 1000
});
var run_parser_tests = require('./parser.js');
run_parser_tests();
/* -----[ utils ]----- */
function tmpl() {