Implement shorthand property assignment in ambiguous contexts.

Issue #181.
This commit is contained in:
Ingvar Stepanyan 2015-01-23 23:41:03 +02:00
parent f0569147e6
commit 65d09eac6e
2 changed files with 348 additions and 55 deletions

138
acorn.js
View File

@ -1468,7 +1468,7 @@
// Convert existing expression atom to assignable pattern // Convert existing expression atom to assignable pattern
// if possible. // if possible.
function toAssignable(node, allowSpread, checkType) { function toAssignable(node) {
if (options.ecmaVersion >= 6 && node) { if (options.ecmaVersion >= 6 && node) {
switch (node.type) { switch (node.type) {
case "Identifier": case "Identifier":
@ -1476,43 +1476,32 @@
case "ObjectPattern": case "ObjectPattern":
case "ArrayPattern": case "ArrayPattern":
case "AssignmentPattern": case "AssignmentPattern":
case "RestElement":
break; break;
case "ObjectExpression": case "ObjectExpression":
node.type = "ObjectPattern"; node.type = "ObjectPattern";
for (var i = 0; i < node.properties.length; i++) { for (var i = 0; i < node.properties.length; i++) {
var prop = node.properties[i]; var prop = node.properties[i];
if (prop.kind !== "init") unexpected(prop.key.start); if (prop.kind !== "init") raise(prop.key.start, "Object pattern can't contain getter or setter");
toAssignable(prop.value, false, checkType); toAssignable(prop.value);
} }
break; break;
case "ArrayExpression": case "ArrayExpression":
node.type = "ArrayPattern"; node.type = "ArrayPattern";
toAssignableList(node.elements, checkType); toAssignableList(node.elements);
break;
case "SpreadElement":
if (allowSpread) {
node.type = "RestElement";
toAssignable(node.argument, false, checkType);
checkSpreadAssign(node.argument);
} else {
unexpected(node.start);
}
break; break;
case "AssignmentExpression": case "AssignmentExpression":
if (node.operator === "=") { if (node.operator === "=") {
node.type = "AssignmentPattern"; node.type = "AssignmentPattern";
} else { } else {
unexpected(node.left.end); raise(node.left.end, "Only '=' operator can be used for specifying default value.");
} }
break; break;
default: default:
if (checkType) unexpected(node.start); raise(node.start, "Assigning to rvalue");
} }
} }
return node; return node;
@ -1520,19 +1509,33 @@
// Convert list of expression atoms to binding list. // Convert list of expression atoms to binding list.
function toAssignableList(exprList, checkType) { function toAssignableList(exprList) {
for (var i = 0; i < exprList.length; i++) { if (exprList.length) {
toAssignable(exprList[i], i === exprList.length - 1, checkType); for (var i = 0; i < exprList.length - 1; i++) {
toAssignable(exprList[i]);
}
var last = exprList[exprList.length - 1];
switch (last.type) {
case "RestElement":
break;
case "SpreadElement":
last.type = "RestElement";
toAssignable(last.argument);
checkSpreadAssign(last.argument);
break;
default:
toAssignable(last);
}
} }
return exprList; return exprList;
} }
// Parses spread element. // Parses spread element.
function parseSpread() { function parseSpread(refShorthandDefaultPos) {
var node = startNode(); var node = startNode();
next(); next();
node.argument = parseMaybeAssign(); node.argument = parseMaybeAssign(refShorthandDefaultPos);
return finishNode(node, "SpreadElement"); return finishNode(node, "SpreadElement");
} }
@ -1837,10 +1840,14 @@
return parseForIn(node, init); return parseForIn(node, init);
return parseFor(node, init); return parseFor(node, init);
} }
var init = parseExpression(true); var refShorthandDefaultPos = {start: 0};
var init = parseExpression(true, refShorthandDefaultPos);
if (tokType === _in || (options.ecmaVersion >= 6 && isContextual("of"))) { if (tokType === _in || (options.ecmaVersion >= 6 && isContextual("of"))) {
toAssignable(init);
checkLVal(init); checkLVal(init);
return parseForIn(node, init); return parseForIn(node, init);
} else if (refShorthandDefaultPos.start) {
unexpected(refShorthandDefaultPos.start);
} }
return parseFor(node, init); return parseFor(node, init);
} }
@ -2069,17 +2076,20 @@
// and, *if* the syntactic construct they handle is present, wrap // and, *if* the syntactic construct they handle is present, wrap
// the AST node that the inner parser gave them in another node. // the AST node that the inner parser gave them in another node.
// Parse a full expression. The arguments are used to forbid comma // Parse a full expression. The optional arguments are used to
// sequences (in argument lists, array literals, or object literals) // forbid the `in` operator (in for loops initalization expressions)
// or the `in` operator (in for loops initalization expressions). // and provide reference for storing '=' operator inside shorthand
// property assignment in contexts where both object expression
// and object pattern might appear (so it's possible to raise
// delayed syntax error at correct position).
function parseExpression(noIn) { function parseExpression(noIn, refShorthandDefaultPos) {
var start = storeCurrentPos(); var start = storeCurrentPos();
var expr = parseMaybeAssign(noIn); var expr = parseMaybeAssign(noIn, refShorthandDefaultPos);
if (tokType === _comma) { if (tokType === _comma) {
var node = startNodeAt(start); var node = startNodeAt(start);
node.expressions = [expr]; node.expressions = [expr];
while (eat(_comma)) node.expressions.push(parseMaybeAssign(noIn)); while (eat(_comma)) node.expressions.push(parseMaybeAssign(noIn, refShorthandDefaultPos));
return finishNode(node, "SequenceExpression"); return finishNode(node, "SequenceExpression");
} }
return expr; return expr;
@ -2088,26 +2098,37 @@
// Parse an assignment expression. This includes applications of // Parse an assignment expression. This includes applications of
// operators like `+=`. // operators like `+=`.
function parseMaybeAssign(noIn) { function parseMaybeAssign(noIn, refShorthandDefaultPos) {
var failOnShorthandAssign;
if (!refShorthandDefaultPos) {
refShorthandDefaultPos = {start: 0};
failOnShorthandAssign = true;
} else {
failOnShorthandAssign = false;
}
var start = storeCurrentPos(); var start = storeCurrentPos();
var left = parseMaybeConditional(noIn); var left = parseMaybeConditional(noIn, refShorthandDefaultPos);
if (tokType.isAssign) { if (tokType.isAssign) {
var node = startNodeAt(start); var node = startNodeAt(start);
node.operator = tokVal; node.operator = tokVal;
node.left = tokType === _eq ? toAssignable(left) : left; node.left = tokType === _eq ? toAssignable(left) : left;
refShorthandDefaultPos.start = 0; // reset because shorthand default was used correctly
checkLVal(left); checkLVal(left);
next(); next();
node.right = parseMaybeAssign(noIn); node.right = parseMaybeAssign(noIn);
return finishNode(node, "AssignmentExpression"); return finishNode(node, "AssignmentExpression");
} else if (failOnShorthandAssign && refShorthandDefaultPos.start) {
unexpected(refShorthandDefaultPos.start);
} }
return left; return left;
} }
// Parse a ternary conditional (`?:`) operator. // Parse a ternary conditional (`?:`) operator.
function parseMaybeConditional(noIn) { function parseMaybeConditional(noIn, refShorthandDefaultPos) {
var start = storeCurrentPos(); var start = storeCurrentPos();
var expr = parseExprOps(noIn); var expr = parseExprOps(noIn, refShorthandDefaultPos);
if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
if (eat(_question)) { if (eat(_question)) {
var node = startNodeAt(start); var node = startNodeAt(start);
node.test = expr; node.test = expr;
@ -2121,9 +2142,11 @@
// Start the precedence parser. // Start the precedence parser.
function parseExprOps(noIn) { function parseExprOps(noIn, refShorthandDefaultPos) {
var start = storeCurrentPos(); var start = storeCurrentPos();
return parseExprOp(parseMaybeUnary(), start, -1, noIn); var expr = parseMaybeUnary(refShorthandDefaultPos);
if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
return parseExprOp(expr, start, -1, noIn);
} }
// Parse binary operators with the operator precedence parsing // Parse binary operators with the operator precedence parsing
@ -2152,13 +2175,14 @@
// Parse unary operators, both prefix and postfix. // Parse unary operators, both prefix and postfix.
function parseMaybeUnary() { function parseMaybeUnary(refShorthandDefaultPos) {
if (tokType.prefix) { if (tokType.prefix) {
var node = startNode(), update = tokType.isUpdate; var node = startNode(), update = tokType.isUpdate;
node.operator = tokVal; node.operator = tokVal;
node.prefix = true; node.prefix = true;
next(); next();
node.argument = parseMaybeUnary(); node.argument = parseMaybeUnary();
if (refShorthandDefaultPos && refShorthandDefaultPos.start) unexpected(refShorthandDefaultPos.start);
if (update) checkLVal(node.argument); if (update) checkLVal(node.argument);
else if (strict && node.operator === "delete" && else if (strict && node.operator === "delete" &&
node.argument.type === "Identifier") node.argument.type === "Identifier")
@ -2166,7 +2190,8 @@
return finishNode(node, update ? "UpdateExpression" : "UnaryExpression"); return finishNode(node, update ? "UpdateExpression" : "UnaryExpression");
} }
var start = storeCurrentPos(); var start = storeCurrentPos();
var expr = parseExprSubscripts(); var expr = parseExprSubscripts(refShorthandDefaultPos);
if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
while (tokType.postfix && !canInsertSemicolon()) { while (tokType.postfix && !canInsertSemicolon()) {
var node = startNodeAt(start); var node = startNodeAt(start);
node.operator = tokVal; node.operator = tokVal;
@ -2181,9 +2206,11 @@
// Parse call, dot, and `[]`-subscript expressions. // Parse call, dot, and `[]`-subscript expressions.
function parseExprSubscripts() { function parseExprSubscripts(refShorthandDefaultPos) {
var start = storeCurrentPos(); var start = storeCurrentPos();
return parseSubscripts(parseExprAtom(), start); var expr = parseExprAtom(refShorthandDefaultPos);
if (refShorthandDefaultPos && refShorthandDefaultPos.start) return expr;
return parseSubscripts(expr, start);
} }
function parseSubscripts(base, start, noCalls) { function parseSubscripts(base, start, noCalls) {
@ -2218,7 +2245,7 @@
// `new`, or an expression wrapped in punctuation like `()`, `[]`, // `new`, or an expression wrapped in punctuation like `()`, `[]`,
// or `{}`. // or `{}`.
function parseExprAtom() { function parseExprAtom(refShorthandDefaultPos) {
switch (tokType) { switch (tokType) {
case _this: case _this:
var node = startNode(); var node = startNode();
@ -2268,11 +2295,11 @@
if (options.ecmaVersion >= 7 && tokType === _for) { if (options.ecmaVersion >= 7 && tokType === _for) {
return parseComprehension(node, false); return parseComprehension(node, false);
} }
node.elements = parseExprList(_bracketR, true, true); node.elements = parseExprList(_bracketR, true, true, refShorthandDefaultPos);
return finishNode(node, "ArrayExpression"); return finishNode(node, "ArrayExpression");
case _braceL: case _braceL:
return parseObj(); return parseObj(false, refShorthandDefaultPos);
case _function: case _function:
var node = startNode(); var node = startNode();
@ -2302,7 +2329,8 @@
return parseComprehension(startNodeAt(start), true); return parseComprehension(startNodeAt(start), true);
} }
var innerStart = storeCurrentPos(), exprList = [], first = true, spreadStart, innerParenStart; var innerStart = storeCurrentPos(), exprList = [], first = true;
var refShorthandDefaultPos = {start: 0}, spreadStart, innerParenStart;
while (tokType !== _parenR) { while (tokType !== _parenR) {
first ? first = false : expect(_comma); first ? first = false : expect(_comma);
if (tokType === _ellipsis) { if (tokType === _ellipsis) {
@ -2313,7 +2341,7 @@
if (tokType === _parenL && !innerParenStart) { if (tokType === _parenL && !innerParenStart) {
innerParenStart = tokStart; innerParenStart = tokStart;
} }
exprList.push(parseMaybeAssign()); exprList.push(parseMaybeAssign(false, refShorthandDefaultPos));
} }
} }
var innerEnd = storeCurrentPos(); var innerEnd = storeCurrentPos();
@ -2326,6 +2354,7 @@
if (!exprList.length) unexpected(lastStart); if (!exprList.length) unexpected(lastStart);
if (spreadStart) unexpected(spreadStart); if (spreadStart) unexpected(spreadStart);
if (refShorthandDefaultPos.start) unexpected(refShorthandDefaultPos.start);
if (exprList.length > 1) { if (exprList.length > 1) {
val = startNodeAt(innerStart); val = startNodeAt(innerStart);
@ -2392,7 +2421,7 @@
// Parse an object literal or binding pattern. // Parse an object literal or binding pattern.
function parseObj(isPattern) { function parseObj(isPattern, refShorthandDefaultPos) {
var node = startNode(), first = true, propHash = {}; var node = startNode(), first = true, propHash = {};
node.properties = []; node.properties = [];
next(); next();
@ -2414,7 +2443,7 @@
} }
parsePropertyName(prop); parsePropertyName(prop);
if (eat(_colon)) { if (eat(_colon)) {
prop.value = isPattern ? parseMaybeDefault(start) : parseMaybeAssign(); prop.value = isPattern ? parseMaybeDefault(start) : parseMaybeAssign(false, refShorthandDefaultPos);
prop.kind = "init"; prop.kind = "init";
} else if (options.ecmaVersion >= 6 && tokType === _parenL) { } else if (options.ecmaVersion >= 6 && tokType === _parenL) {
if (isPattern) unexpected(); if (isPattern) unexpected();
@ -2430,7 +2459,15 @@
prop.value = parseMethod(false); prop.value = parseMethod(false);
} else if (options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") { } else if (options.ecmaVersion >= 6 && !prop.computed && prop.key.type === "Identifier") {
prop.kind = "init"; prop.kind = "init";
prop.value = isPattern ? parseMaybeDefault(start, prop.key) : prop.key; if (isPattern) {
prop.value = parseMaybeDefault(start, prop.key);
} else if (tokType === _eq && refShorthandDefaultPos) {
if (!refShorthandDefaultPos.start)
refShorthandDefaultPos.start = tokStart;
prop.value = parseMaybeDefault(start, prop.key);
} else {
prop.value = prop.key;
}
prop.shorthand = true; prop.shorthand = true;
} else unexpected(); } else unexpected();
@ -2583,7 +2620,7 @@
// nothing in between them to be parsed as `null` (which is needed // nothing in between them to be parsed as `null` (which is needed
// for array literals). // for array literals).
function parseExprList(close, allowTrailingComma, allowEmpty) { function parseExprList(close, allowTrailingComma, allowEmpty, refShorthandDefaultPos) {
var elts = [], first = true; var elts = [], first = true;
while (!eat(close)) { while (!eat(close)) {
if (!first) { if (!first) {
@ -2594,7 +2631,10 @@
if (allowEmpty && tokType === _comma) { if (allowEmpty && tokType === _comma) {
elts.push(null); elts.push(null);
} else { } else {
elts.push(tokType === _ellipsis ? parseSpread() : parseMaybeAssign()); if (tokType === _ellipsis)
elts.push(parseSpread(refShorthandDefaultPos));
else
elts.push(parseMaybeAssign(false, refShorthandDefaultPos));
} }
} }
return elts; return elts;

View File

@ -13745,7 +13745,7 @@ testFail("[2] = 42", "Assigning to rvalue (1:1)", {ecmaVersion: 6});
testFail("({ obj:20 }) = 42", "Assigning to rvalue (1:7)", {ecmaVersion: 6}); testFail("({ obj:20 }) = 42", "Assigning to rvalue (1:7)", {ecmaVersion: 6});
testFail("( { get x() {} } ) = 0", "Unexpected token (1:8)", {ecmaVersion: 6}); testFail("( { get x() {} } ) = 0", "Object pattern can't contain getter or setter (1:8)", {ecmaVersion: 6});
testFail("x \n is y", "Unexpected token (2:4)", {ecmaVersion: 6}); testFail("x \n is y", "Unexpected token (2:4)", {ecmaVersion: 6});
@ -13801,9 +13801,9 @@ testFail("\"use strict\"; (a) => 00", "Invalid number (1:21)", {ecmaVersion: 6})
testFail("() <= 42", "Unexpected token (1:1)", {ecmaVersion: 6}); testFail("() <= 42", "Unexpected token (1:1)", {ecmaVersion: 6});
testFail("(10) => 00", "Unexpected token (1:1)", {ecmaVersion: 6}); testFail("(10) => 00", "Assigning to rvalue (1:1)", {ecmaVersion: 6});
testFail("(10, 20) => 00", "Unexpected token (1:1)", {ecmaVersion: 6}); testFail("(10, 20) => 00", "Assigning to rvalue (1:1)", {ecmaVersion: 6});
testFail("yield v", "Unexpected token (1:6)", {ecmaVersion: 6}); testFail("yield v", "Unexpected token (1:6)", {ecmaVersion: 6});
@ -14037,7 +14037,7 @@ testFail("\"use strict\"; function x(a, ...[a]){}", "Argument name clash in stri
testFail("(...a, b) => {}", "Unexpected token (1:5)", {ecmaVersion: 6}); testFail("(...a, b) => {}", "Unexpected token (1:5)", {ecmaVersion: 6});
testFail("([ 5 ]) => {}", "Unexpected token (1:3)", {ecmaVersion: 6}); testFail("([ 5 ]) => {}", "Assigning to rvalue (1:3)", {ecmaVersion: 6});
testFail("({ 5 }) => {}", "Unexpected token (1:5)", {ecmaVersion: 6}); testFail("({ 5 }) => {}", "Unexpected token (1:5)", {ecmaVersion: 6});
@ -14045,7 +14045,7 @@ testFail("(...[ 5 ]) => {}", "Unexpected token (1:6)", {ecmaVersion: 6});
testFail("[...{ a }] = b", "Unexpected token (1:4)", {ecmaVersion: 6}); testFail("[...{ a }] = b", "Unexpected token (1:4)", {ecmaVersion: 6});
testFail("[...a, b] = c", "Unexpected token (1:1)", {ecmaVersion: 6}); testFail("[...a, b] = c", "Assigning to rvalue (1:1)", {ecmaVersion: 6});
testFail("({ t(eval) { \"use strict\"; } });", "Defining 'eval' in strict mode (1:5)", {ecmaVersion: 6}); testFail("({ t(eval) { \"use strict\"; } });", "Defining 'eval' in strict mode (1:5)", {ecmaVersion: 6});
@ -14120,7 +14120,7 @@ testFail("\"use strict\"; (eval) => 42", "Defining 'eval' in strict mode (1:15)"
testFail("(eval) => { \"use strict\"; 42 }", "Defining 'eval' in strict mode (1:1)", {ecmaVersion: 6}); testFail("(eval) => { \"use strict\"; 42 }", "Defining 'eval' in strict mode (1:1)", {ecmaVersion: 6});
testFail("({ get test() { } }) => 42", "Unexpected token (1:7)", {ecmaVersion: 6}); testFail("({ get test() { } }) => 42", "Object pattern can't contain getter or setter (1:7)", {ecmaVersion: 6});
/* Regression tests */ /* Regression tests */
@ -14509,6 +14509,259 @@ test("var [localVar = defaultValue] = obj", {
loose: false loose: false
}); });
test("({x = 0} = obj)", {
type: "Program",
range: [0, 15],
body: [{
type: "ExpressionStatement",
range: [0, 15],
expression: {
type: "AssignmentExpression",
range: [1, 14],
operator: "=",
left: {
type: "ObjectPattern",
range: [1, 8],
properties: [{
type: "Property",
range: [2, 7],
method: false,
shorthand: true,
computed: false,
key: {
type: "Identifier",
range: [2, 3],
name: "x"
},
kind: "init",
value: {
type: "AssignmentPattern",
range: [6, 7],
operator: "=",
left: {
type: "Identifier",
range: [2, 3],
name: "x"
},
right: {
type: "Literal",
range: [6, 7],
value: 0
}
}
}]
},
right: {
type: "Identifier",
range: [11, 14],
name: "obj"
}
}
}]
}, {
ecmaVersion: 6,
ranges: true,
loose: false
});
test("({x = 0}) => x", {
type: "Program",
range: [0, 14],
body: [{
type: "ExpressionStatement",
range: [0, 14],
expression: {
type: "ArrowFunctionExpression",
range: [0, 14],
id: null,
generator: false,
expression: true,
params: [{
type: "ObjectPattern",
range: [1, 8],
properties: [{
type: "Property",
range: [2, 7],
method: false,
shorthand: true,
computed: false,
key: {
type: "Identifier",
range: [2, 3],
name: "x"
},
kind: "init",
value: {
type: "AssignmentPattern",
range: [6, 7],
operator: "=",
left: {
type: "Identifier",
range: [2, 3],
name: "x"
},
right: {
type: "Literal",
range: [6, 7],
value: 0
}
}
}]
}],
body: {
type: "Identifier",
range: [13, 14],
name: "x"
}
}
}]
}, {
ecmaVersion: 6,
ranges: true,
loose: false
});
test("[a, {b: {c = 1}}] = arr", {
type: "Program",
range: [0, 23],
body: [{
type: "ExpressionStatement",
range: [0, 23],
expression: {
type: "AssignmentExpression",
range: [0, 23],
operator: "=",
left: {
type: "ArrayPattern",
range: [0, 17],
elements: [
{
type: "Identifier",
range: [1, 2],
name: "a"
},
{
type: "ObjectPattern",
range: [4, 16],
properties: [{
type: "Property",
range: [5, 15],
method: false,
shorthand: false,
computed: false,
key: {
type: "Identifier",
range: [5, 6],
name: "b"
},
value: {
type: "ObjectPattern",
range: [8, 15],
properties: [{
type: "Property",
range: [9, 14],
method: false,
shorthand: true,
computed: false,
key: {
type: "Identifier",
range: [9, 10],
name: "c"
},
kind: "init",
value: {
type: "AssignmentPattern",
range: [13, 14],
operator: "=",
left: {
type: "Identifier",
range: [9, 10],
name: "c"
},
right: {
type: "Literal",
range: [13, 14],
value: 1
}
}
}]
},
kind: "init"
}]
}
]
},
right: {
type: "Identifier",
range: [20, 23],
name: "arr"
}
}
}]
}, {
ecmaVersion: 6,
ranges: true,
loose: false
});
test("for ({x = 0} in arr);", {
type: "Program",
range: [0, 21],
body: [{
type: "ForInStatement",
range: [0, 21],
left: {
type: "ObjectPattern",
range: [5, 12],
properties: [{
type: "Property",
range: [6, 11],
method: false,
shorthand: true,
computed: false,
key: {
type: "Identifier",
range: [6, 7],
name: "x"
},
kind: "init",
value: {
type: "AssignmentPattern",
range: [10, 11],
operator: "=",
left: {
type: "Identifier",
range: [6, 7],
name: "x"
},
right: {
type: "Literal",
range: [10, 11],
value: 0
}
}
}]
},
right: {
type: "Identifier",
range: [16, 19],
name: "arr"
},
body: {
type: "EmptyStatement",
range: [20, 21]
}
}]
}, {
ecmaVersion: 6,
ranges: true,
loose: false
});
testFail("obj = {x = 0}", "Unexpected token (1:9)", {ecmaVersion: 6});
testFail("f({x = 0})", "Unexpected token (1:5)", {ecmaVersion: 6});
// https://github.com/marijnh/acorn/issues/191 // https://github.com/marijnh/acorn/issues/191
test("try {} catch ({message}) {}", { test("try {} catch ({message}) {}", {