support destructured literals (#4278)

This commit is contained in:
Alex Lam S.L
2020-11-17 00:01:24 +00:00
committed by GitHub
parent 42e34c870a
commit e5f80afc53
10 changed files with 2477 additions and 305 deletions

View File

@@ -414,8 +414,12 @@ var AST_ForIn = DEFNODE("ForIn", "init object", {
_validate: function() {
if (this.init instanceof AST_Definitions) {
if (this.init.definitions.length != 1) throw new Error("init must have single declaration");
} else if (!(this.init instanceof AST_PropAccess || this.init instanceof AST_SymbolRef)) {
throw new Error("init must be assignable");
} else {
validate_destructured(this.init, function(node) {
if (!(node instanceof AST_PropAccess || node instanceof AST_SymbolRef)) {
throw new Error("init must be assignable: " + node.TYPE);
}
});
}
must_be_expression(this, "object");
},
@@ -496,12 +500,26 @@ var AST_Toplevel = DEFNODE("Toplevel", "globals", {
}
}, AST_Scope);
var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments length_read", {
var AST_Lambda = DEFNODE("Lambda", "name argnames length_read uses_arguments", {
$documentation: "Base class for functions",
$propdoc: {
name: "[AST_SymbolDeclaration?] the name of this function",
argnames: "[AST_SymbolFunarg*] array of function arguments",
uses_arguments: "[boolean/S] tells whether this function accesses the arguments array"
argnames: "[(AST_Destructured|AST_SymbolFunarg)*] array of function arguments and/or destructured literals",
uses_arguments: "[boolean/S] tells whether this function accesses the arguments array",
},
each_argname: function(visit) {
var tw = new TreeWalker(function(node) {
if (node instanceof AST_DestructuredObject) {
node.properties.forEach(function(prop) {
prop.value.walk(tw);
});
return true;
}
if (node instanceof AST_SymbolFunarg) visit(node);
});
this.argnames.forEach(function(argname) {
argname.walk(tw);
});
},
walk: function(visitor) {
var node = this;
@@ -515,7 +533,9 @@ var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments length_read", {
},
_validate: function() {
this.argnames.forEach(function(node) {
if (!(node instanceof AST_SymbolFunarg)) throw new Error("argnames must be AST_SymbolFunarg[]");
validate_destructured(node, function(node) {
if (!(node instanceof AST_SymbolFunarg)) throw new Error("argnames must be AST_SymbolFunarg[]");
});
});
},
}, AST_Scope);
@@ -748,8 +768,10 @@ var AST_Const = DEFNODE("Const", null, {
_validate: function() {
this.definitions.forEach(function(node) {
if (!(node instanceof AST_VarDef)) throw new Error("definitions must be AST_VarDef[]");
if (!(node.name instanceof AST_SymbolConst)) throw new Error("name must be AST_SymbolConst");
must_be_expression(node, "value");
validate_destructured(node.name, function(node) {
if (!(node instanceof AST_SymbolConst)) throw new Error("name must be AST_SymbolConst");
});
if (node.value != null) must_be_expression(node, "value");
});
},
}, AST_Definitions);
@@ -759,7 +781,9 @@ var AST_Let = DEFNODE("Let", null, {
_validate: function() {
this.definitions.forEach(function(node) {
if (!(node instanceof AST_VarDef)) throw new Error("definitions must be AST_VarDef[]");
if (!(node.name instanceof AST_SymbolLet)) throw new Error("name must be AST_SymbolLet");
validate_destructured(node.name, function(node) {
if (!(node instanceof AST_SymbolLet)) throw new Error("name must be AST_SymbolLet");
});
if (node.value != null) must_be_expression(node, "value");
});
},
@@ -770,7 +794,9 @@ var AST_Var = DEFNODE("Var", null, {
_validate: function() {
this.definitions.forEach(function(node) {
if (!(node instanceof AST_VarDef)) throw new Error("definitions must be AST_VarDef[]");
if (!(node.name instanceof AST_SymbolVar)) throw new Error("name must be AST_SymbolVar");
validate_destructured(node.name, function(node) {
if (!(node instanceof AST_SymbolVar)) throw new Error("name must be AST_SymbolVar");
});
if (node.value != null) must_be_expression(node, "value");
});
},
@@ -969,6 +995,14 @@ var AST_Assign = DEFNODE("Assign", null, {
$documentation: "An assignment expression — `a = b + 5`",
_validate: function() {
if (this.operator.indexOf("=") < 0) throw new Error('operator must contain "="');
if (this.left instanceof AST_Destructured) {
if (this.operator != "=") throw new Error("invalid destructuring operator: " + this.operator);
validate_destructured(this.left, function(node) {
if (!(node instanceof AST_PropAccess || node instanceof AST_SymbolRef)) {
throw new Error("left must be assignable: " + node.TYPE);
}
});
}
},
}, AST_Binary);
@@ -992,6 +1026,77 @@ var AST_Array = DEFNODE("Array", "elements", {
},
});
var AST_Destructured = DEFNODE("Destructured", null, {
$documentation: "Base class for destructured literal",
});
function validate_destructured(node, check) {
if (node instanceof AST_DestructuredArray) return node.elements.forEach(function(node) {
if (!(node instanceof AST_Hole)) validate_destructured(node, check);
});
if (node instanceof AST_DestructuredObject) return node.properties.forEach(function(prop) {
validate_destructured(prop.value, check);
});
check(node);
}
var AST_DestructuredArray = DEFNODE("DestructuredArray", "elements", {
$documentation: "A destructured array literal",
$propdoc: {
elements: "[AST_Node*] array of elements",
},
walk: function(visitor) {
var node = this;
visitor.visit(node, function() {
node.elements.forEach(function(element) {
element.walk(visitor);
});
});
},
}, AST_Destructured);
var AST_DestructuredKeyVal = DEFNODE("DestructuredKeyVal", "key value", {
$documentation: "A key: value destructured property",
$propdoc: {
key: "[string|AST_Node] property name. For computed property this is an AST_Node.",
value: "[AST_Node] property value",
},
walk: function(visitor) {
var node = this;
visitor.visit(node, function() {
if (node.key instanceof AST_Node) node.key.walk(visitor);
node.value.walk(visitor);
});
},
_validate: function() {
if (typeof this.key != "string") {
if (!(this.key instanceof AST_Node)) throw new Error("key must be string or AST_Node");
must_be_expression(this, "key");
}
must_be_expression(this, "value");
},
});
var AST_DestructuredObject = DEFNODE("DestructuredObject", "properties", {
$documentation: "A destructured object literal",
$propdoc: {
properties: "[AST_DestructuredKeyVal*] array of properties",
},
walk: function(visitor) {
var node = this;
visitor.visit(node, function() {
node.properties.forEach(function(prop) {
prop.walk(visitor);
});
});
},
_validate: function() {
this.properties.forEach(function(node) {
if (!(node instanceof AST_DestructuredKeyVal)) throw new Error("properties must be AST_DestructuredKeyVal[]");
});
},
}, AST_Destructured);
var AST_Object = DEFNODE("Object", "properties", {
$documentation: "An object literal",
$propdoc: {

File diff suppressed because it is too large Load Diff

View File

@@ -69,6 +69,7 @@ function OutputStream(options) {
semicolons : true,
shebang : true,
source_map : null,
v8 : false,
webkit : false,
width : 80,
wrap_iife : false,
@@ -499,11 +500,11 @@ function OutputStream(options) {
}
}
if (/comment[134]/.test(c.type)) {
print("//" + c.value.replace(/[@#]__PURE__/g, ' ') + "\n");
print("//" + c.value.replace(/[@#]__PURE__/g, " ") + "\n");
indent();
last_nlb = true;
} else if (c.type == "comment2") {
print("/*" + c.value.replace(/[@#]__PURE__/g, ' ') + "*/");
print("/*" + c.value.replace(/[@#]__PURE__/g, " ") + "*/");
last_nlb = false;
}
});
@@ -557,10 +558,10 @@ function OutputStream(options) {
space();
}
if (/comment[134]/.test(c.type)) {
print("//" + c.value.replace(/[@#]__PURE__/g, ' '));
print("//" + c.value.replace(/[@#]__PURE__/g, " "));
need_newline_indented = true;
} else if (c.type == "comment2") {
print("/*" + c.value.replace(/[@#]__PURE__/g, ' ') + "*/");
print("/*" + c.value.replace(/[@#]__PURE__/g, " ") + "*/");
need_space = true;
}
});
@@ -610,7 +611,7 @@ function OutputStream(options) {
},
parent : function(n) {
return stack[stack.length - 2 - (n || 0)];
}
},
};
}
@@ -652,13 +653,7 @@ function OutputStream(options) {
/* -----[ PARENTHESES ]----- */
function PARENS(nodetype, func) {
if (Array.isArray(nodetype)) {
nodetype.forEach(function(nodetype) {
PARENS(nodetype, func);
});
} else {
nodetype.DEFMETHOD("needs_parens", func);
}
nodetype.DEFMETHOD("needs_parens", func);
}
PARENS(AST_Node, return_false);
@@ -667,11 +662,11 @@ function OutputStream(options) {
// the first token to appear in a statement.
PARENS(AST_Function, function(output) {
if (!output.has_parens() && first_in_statement(output)) return true;
if (output.option('webkit')) {
if (output.option("webkit")) {
var p = output.parent();
if (p instanceof AST_PropAccess && p.expression === this) return true;
}
if (output.option('wrap_iife')) {
if (output.option("wrap_iife")) {
var p = output.parent();
if (p instanceof AST_Call && p.expression === this) return true;
}
@@ -679,9 +674,10 @@ function OutputStream(options) {
// same goes for an object literal, because otherwise it would be
// interpreted as a block of code.
PARENS(AST_Object, function(output) {
function needs_parens_obj(output) {
return !output.has_parens() && first_in_statement(output);
});
}
PARENS(AST_Object, needs_parens_obj);
PARENS(AST_Unary, function(output) {
var p = output.parent();
@@ -701,6 +697,7 @@ function OutputStream(options) {
|| p instanceof AST_Conditional
// { [(1, 2)]: 3 }[2] ==> 3
// { foo: (1, 2) }.foo ==> 2
|| p instanceof AST_DestructuredKeyVal
|| p instanceof AST_ObjectProperty
// (1, {foo:2}).foo or (1, {foo:2})["foo"] ==> 2
|| p instanceof AST_PropAccess && p.expression === this
@@ -747,7 +744,7 @@ function OutputStream(options) {
var p = output.parent();
if (p instanceof AST_New) return p.expression === this;
// https://bugs.webkit.org/show_bug.cgi?id=123506
if (output.option('webkit')) {
if (output.option("webkit")) {
var g = output.parent(1);
return this.expression instanceof AST_Function
&& p instanceof AST_PropAccess
@@ -776,18 +773,29 @@ function OutputStream(options) {
}
});
PARENS([ AST_Assign, AST_Conditional ], function(output) {
function needs_parens_assign_cond(self, output) {
var p = output.parent();
// 1 + (a = 2) + 3 → 6, side effect setting a = 2
if (p instanceof AST_Binary) return !(p instanceof AST_Assign);
// (a = func)() —or— new (a = Object)()
if (p instanceof AST_Call) return p.expression === this;
if (p instanceof AST_Call) return p.expression === self;
// (a = foo) ? bar : baz
if (p instanceof AST_Conditional) return p.condition === this;
if (p instanceof AST_Conditional) return p.condition === self;
// (a = foo)["prop"] —or— (a = foo).prop
if (p instanceof AST_PropAccess) return p.expression === this;
if (p instanceof AST_PropAccess) return p.expression === self;
// !(a = false) → true
if (p instanceof AST_Unary) return true;
}
PARENS(AST_Assign, function(output) {
if (needs_parens_assign_cond(this, output)) return true;
// v8 parser bug => workaround
// f([1], [a] = []) => f([1], ([a] = []))
if (output.option("v8")) return this.left instanceof AST_Destructured;
// ({ p: a } = o);
if (this.left instanceof AST_DestructuredObject) return needs_parens_obj(output);
});
PARENS(AST_Conditional, function(output) {
return needs_parens_assign_cond(this, output);
});
/* -----[ PRINTERS ]----- */
@@ -1274,6 +1282,38 @@ function OutputStream(options) {
output.space();
} : noop);
});
DEFPRINT(AST_DestructuredArray, function(output) {
var a = this.elements, len = a.length;
output.with_square(len > 0 ? function() {
output.space();
a.forEach(function(exp, i) {
if (i) output.comma();
exp.print(output);
// If the final element is a hole, we need to make sure it
// doesn't look like a trailing comma, by inserting an actual
// trailing comma.
if (i === len - 1 && exp instanceof AST_Hole)
output.comma();
});
output.space();
} : noop);
});
DEFPRINT(AST_DestructuredKeyVal, print_key_value);
DEFPRINT(AST_DestructuredObject, function(output) {
var props = this.properties;
if (props.length > 0) output.with_block(function() {
props.forEach(function(prop, i) {
if (i) {
output.print(",");
output.newline();
}
output.indent();
prop.print(output);
});
output.newline();
});
else print_braced_empty(this, output);
});
DEFPRINT(AST_Object, function(output) {
var props = this.properties;
if (props.length > 0) output.with_block(function() {
@@ -1314,12 +1354,13 @@ function OutputStream(options) {
}
}
DEFPRINT(AST_ObjectKeyVal, function(output) {
function print_key_value(output) {
var self = this;
print_property_key(self, output);
output.colon();
self.value.print(output);
});
}
DEFPRINT(AST_ObjectKeyVal, print_key_value);
function print_accessor(type) {
return function(output) {
var self = this;
@@ -1483,6 +1524,7 @@ function OutputStream(options) {
AST_Constant,
AST_Debugger,
AST_Definitions,
AST_Destructured,
AST_Finally,
AST_Jump,
AST_Lambda,
@@ -1497,7 +1539,7 @@ function OutputStream(options) {
output.add_mapping(this.start);
});
DEFMAP([ AST_ObjectProperty ], function(output) {
DEFMAP([ AST_DestructuredKeyVal, AST_ObjectProperty ], function(output) {
if (typeof this.key == "string") output.add_mapping(this.start, this.key);
});
})();

View File

@@ -973,14 +973,16 @@ function parse($TEXT, options) {
if (!is("punc", ";")) {
init = is("keyword", "const")
? (next(), const_(true))
: is("keyword", "let")
? (next(), let_(true))
: is("keyword", "var")
? (next(), var_(true))
: expression(true, true);
if (is("operator", "in")) {
if (init instanceof AST_Var) {
if (init instanceof AST_Definitions) {
if (init.definitions.length > 1)
croak("Only one variable declaration allowed in for..in loop", init.start.line, init.start.col, init.start.pos);
} else if (!is_assignable(init)) {
} else if (!(is_assignable(init) || (init = to_destructured(init)) instanceof AST_Destructured)) {
croak("Invalid left-hand side in for..in loop", init.start.line, init.start.col, init.start.pos);
}
next();
@@ -1025,7 +1027,7 @@ function parse($TEXT, options) {
var argnames = [];
for (var first = true; !is("punc", ")");) {
if (first) first = false; else expect(",");
argnames.push(as_symbol(AST_SymbolFunarg));
argnames.push(maybe_destructured(AST_SymbolFunarg));
}
next();
var loop = S.in_loop;
@@ -1146,16 +1148,16 @@ function parse($TEXT, options) {
});
}
function vardefs(type, no_in, must_init) {
function vardefs(type, no_in) {
var a = [];
for (;;) {
var start = S.token;
var name = as_symbol(type);
var name = maybe_destructured(type);
var value = null;
if (is("operator", "=")) {
next();
value = expression(false, no_in);
} else if (must_init) {
} else if (!no_in && (type === AST_SymbolConst || name instanceof AST_Destructured)) {
croak("Missing initializer in declaration");
}
a.push(new AST_VarDef({
@@ -1174,7 +1176,7 @@ function parse($TEXT, options) {
var const_ = function(no_in) {
return new AST_Const({
start : prev(),
definitions : vardefs(AST_SymbolConst, no_in, true),
definitions : vardefs(AST_SymbolConst, no_in),
end : prev()
});
};
@@ -1306,7 +1308,8 @@ function parse($TEXT, options) {
unexpected();
};
function expr_list(closing, allow_trailing_comma, allow_empty) {
function expr_list(closing, allow_trailing_comma, allow_empty, parser) {
if (!parser) parser = expression;
var first = true, a = [];
while (!is("punc", closing)) {
if (first) first = false; else expect(",");
@@ -1314,7 +1317,7 @@ function parse($TEXT, options) {
if (is("punc", ",") && allow_empty) {
a.push(new AST_Hole({ start: S.token, end: S.token }));
} else {
a.push(expression(false));
a.push(parser());
}
}
next();
@@ -1449,6 +1452,54 @@ function parse($TEXT, options) {
return sym;
}
function maybe_destructured(type) {
var start = S.token;
if (is("punc", "[")) {
next();
return new AST_DestructuredArray({
start: start,
elements: expr_list("]", !options.strict, true, function() {
return maybe_destructured(type);
}),
end: prev(),
});
}
if (is("punc", "{")) {
next();
var first = true, a = [];
while (!is("punc", "}")) {
if (first) first = false; else expect(",");
// allow trailing comma
if (!options.strict && is("punc", "}")) break;
var key_start = S.token;
var key = as_property_key();
if (!is("punc", ":") && key_start.type == "name") {
a.push(new AST_DestructuredKeyVal({
start: key_start,
key: key,
value: _make_symbol(type, key_start),
end: prev(),
}));
continue;
}
expect(":");
a.push(new AST_DestructuredKeyVal({
start: key_start,
key: key,
value: maybe_destructured(type),
end: prev(),
}));
}
next();
return new AST_DestructuredObject({
start: start,
properties: a,
end: prev(),
});
}
return as_symbol(type);
}
function mark_pure(call) {
var start = call.start;
var comments = start.comments_before;
@@ -1578,11 +1629,43 @@ function parse($TEXT, options) {
return expr instanceof AST_PropAccess || expr instanceof AST_SymbolRef;
}
function to_destructured(node) {
if (node instanceof AST_Array) {
var elements = node.elements.map(to_destructured);
return all(elements, function(node) {
return node instanceof AST_Destructured || node instanceof AST_Hole || is_assignable(node);
}) ? new AST_DestructuredArray({
start: node.start,
elements: elements,
end: node.end,
}) : node;
}
if (!(node instanceof AST_Object)) return node;
var props = [];
for (var i = 0; i < node.properties.length; i++) {
var prop = node.properties[i];
if (!(prop instanceof AST_ObjectKeyVal)) return node;
var value = to_destructured(prop.value);
if (!(value instanceof AST_Destructured || is_assignable(value))) return node;
props.push(new AST_DestructuredKeyVal({
start: prop.start,
key: prop.key,
value: value,
end: prop.end,
}));
}
return new AST_DestructuredObject({
start: node.start,
properties: props,
end: node.end,
});
}
var maybe_assign = function(no_in) {
var start = S.token;
var left = maybe_conditional(no_in), val = S.token.value;
if (is("operator") && ASSIGNMENT[val]) {
if (is_assignable(left)) {
if (is_assignable(left) || val == "=" && (left = to_destructured(left)) instanceof AST_Destructured) {
next();
return new AST_Assign({
start : start,

View File

@@ -204,7 +204,17 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
// pass 2: find back references and eval
self.globals = new Dictionary();
var in_arg = [];
var tw = new TreeWalker(function(node) {
if (node instanceof AST_Lambda) {
in_arg.push(node);
node.argnames.forEach(function(argname) {
argname.walk(tw);
});
in_arg.pop();
walk_body(node, tw);
return true;
}
if (node instanceof AST_LoopControl) {
if (node.label) node.label.thedef.references.push(node);
return true;
@@ -212,6 +222,16 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
if (node instanceof AST_SymbolRef) {
var name = node.name;
var sym = node.scope.find_variable(name);
for (var i = in_arg.length; i > 0 && sym;) {
i = in_arg.lastIndexOf(sym.scope, i - 1);
if (i < 0) break;
var decl = sym.orig[0];
if (decl instanceof AST_SymbolFunarg || decl instanceof AST_SymbolLambda) {
node.in_arg = true;
break;
}
sym = sym.scope.parent_scope.find_variable(name);
}
if (!sym) {
sym = self.def_global(node);
} else if (name == "arguments" && sym.scope instanceof AST_Lambda) {

View File

@@ -160,6 +160,16 @@ TreeTransformer.prototype = new TreeWalker;
DEF(AST_Array, function(self, tw) {
self.elements = do_list(self.elements, tw);
});
DEF(AST_DestructuredArray, function(self, tw) {
self.elements = do_list(self.elements, tw);
});
DEF(AST_DestructuredKeyVal, function(self, tw) {
if (self.key instanceof AST_Node) self.key = self.key.transform(tw);
self.value = self.value.transform(tw);
});
DEF(AST_DestructuredObject, function(self, tw) {
self.properties = do_list(self.properties, tw);
});
DEF(AST_Object, function(self, tw) {
self.properties = do_list(self.properties, tw);
});