Disallow trailing comma after rest (#9311)

* Add new tests

* Use state instead of param and disallow comma in [...a,]=[]

* Unify error messages

* Object destructuring

* Update whitelist
This commit is contained in:
Nicolò Ribaudo
2019-01-11 13:08:38 +01:00
committed by GitHub
parent 28b70e5910
commit 9764718c32
31 changed files with 312 additions and 262 deletions

View File

@@ -130,6 +130,9 @@ export default class ExpressionParser extends LValParser {
return left;
}
const oldCommaAfterSpreadAt = this.state.commaAfterSpreadAt;
this.state.commaAfterSpreadAt = -1;
let failOnShorthandAssign;
if (refShorthandDefaultPos) {
failOnShorthandAssign = false;
@@ -169,21 +172,26 @@ export default class ExpressionParser extends LValParser {
this.checkLVal(left, undefined, undefined, "assignment expression");
if (left.extra && left.extra.parenthesized) {
let errorMsg;
if (left.type === "ObjectPattern") {
errorMsg = "`({a}) = 0` use `({a} = 0)`";
} else if (left.type === "ArrayPattern") {
errorMsg = "`([a]) = 0` use `([a] = 0)`";
}
if (errorMsg) {
this.raise(
left.start,
`You're trying to assign to a parenthesized expression, eg. instead of ${errorMsg}`,
);
}
let patternErrorMsg;
let elementName;
if (left.type === "ObjectPattern") {
patternErrorMsg = "`({a}) = 0` use `({a} = 0)`";
elementName = "property";
} else if (left.type === "ArrayPattern") {
patternErrorMsg = "`([a]) = 0` use `([a] = 0)`";
elementName = "element";
}
if (patternErrorMsg && left.extra && left.extra.parenthesized) {
this.raise(
left.start,
`You're trying to assign to a parenthesized expression, eg. instead of ${patternErrorMsg}`,
);
}
if (elementName) this.checkCommaAfterRestFromSpread(elementName);
this.state.commaAfterSpreadAt = oldCommaAfterSpreadAt;
this.next();
node.right = this.parseMaybeAssign(noIn);
return this.finishNode(node, "AssignmentExpression");
@@ -191,6 +199,8 @@ export default class ExpressionParser extends LValParser {
this.unexpected(refShorthandDefaultPos.start);
}
this.state.commaAfterSpreadAt = oldCommaAfterSpreadAt;
return left;
}
@@ -580,15 +590,12 @@ export default class ExpressionParser extends LValParser {
let node = this.startNodeAt(startPos, startLoc);
node.callee = base;
// TODO: Clean up/merge this into `this.state` or a class like acorn's
// `DestructuringErrors` alongside refShorthandDefaultPos and
// refNeedsArrowPos.
const refTrailingCommaPos: Pos = { start: -1 };
const oldCommaAfterSpreadAt = this.state.commaAfterSpreadAt;
this.state.commaAfterSpreadAt = -1;
node.arguments = this.parseCallExpressionArguments(
tt.parenR,
possibleAsync,
refTrailingCommaPos,
);
if (!state.optionalChainMember) {
this.finishCallExpression(node);
@@ -599,12 +606,7 @@ export default class ExpressionParser extends LValParser {
if (possibleAsync && this.shouldParseAsyncArrow()) {
state.stop = true;
if (refTrailingCommaPos.start > -1) {
this.raise(
refTrailingCommaPos.start,
"A trailing comma is not permitted after the rest element",
);
}
this.checkCommaAfterRestFromSpread("parameter");
node = this.parseAsyncArrowFromCallExpression(
this.startNodeAt(startPos, startLoc),
@@ -621,6 +623,7 @@ export default class ExpressionParser extends LValParser {
}
this.state.maybeInArrowParameters = oldMaybeInArrowParameters;
this.state.commaAfterSpreadAt = oldCommaAfterSpreadAt;
return node;
} else if (this.match(tt.backQuote)) {
@@ -700,7 +703,6 @@ export default class ExpressionParser extends LValParser {
parseCallExpressionArguments(
close: TokenType,
possibleAsyncArrow: boolean,
refTrailingCommaPos?: Pos,
): $ReadOnlyArray<?N.Expression> {
const elts = [];
let innerParenStart;
@@ -725,7 +727,6 @@ export default class ExpressionParser extends LValParser {
false,
possibleAsyncArrow ? { start: 0 } : undefined,
possibleAsyncArrow ? { start: 0 } : undefined,
possibleAsyncArrow ? refTrailingCommaPos : undefined,
),
);
}
@@ -1192,14 +1193,7 @@ export default class ExpressionParser extends LValParser {
),
);
if (this.match(tt.comma)) {
const nextTokenType = this.lookahead().type;
const errorMessage =
nextTokenType === tt.parenR
? "A trailing comma is not permitted after the rest element"
: "Rest parameter must be last formal parameter";
this.raise(this.state.start, errorMessage);
}
this.checkCommaAfterRest(tt.parenR, "parameter");
break;
} else {
@@ -1398,8 +1392,6 @@ export default class ExpressionParser extends LValParser {
node.properties = [];
this.next();
let firstRestLocation = null;
while (!this.eat(tt.braceR)) {
if (first) {
first = false;
@@ -1435,34 +1427,14 @@ export default class ExpressionParser extends LValParser {
if (this.match(tt.ellipsis)) {
prop = this.parseSpread(isPattern ? { start: 0 } : undefined);
if (isPattern) {
this.toAssignable(prop, true, "object pattern");
}
node.properties.push(prop);
if (isPattern) {
const position = this.state.start;
if (firstRestLocation !== null) {
this.unexpected(
firstRestLocation,
"Cannot have multiple rest elements when destructuring",
);
} else if (this.eat(tt.braceR)) {
break;
} else if (
this.match(tt.comma) &&
this.lookahead().type === tt.braceR
) {
this.unexpected(
position,
"A trailing comma is not permitted after the rest element",
);
} else {
firstRestLocation = position;
continue;
}
} else {
continue;
this.toAssignable(prop, true, "object pattern");
this.checkCommaAfterRest(tt.braceR, "property");
this.expect(tt.braceR);
break;
}
continue;
}
prop.method = false;
@@ -1519,13 +1491,6 @@ export default class ExpressionParser extends LValParser {
node.properties.push(prop);
}
if (firstRestLocation !== null) {
this.unexpected(
firstRestLocation,
"The rest element has to be the last element when destructuring",
);
}
if (decorators.length) {
this.raise(
this.state.start,
@@ -1923,7 +1888,6 @@ export default class ExpressionParser extends LValParser {
allowEmpty: ?boolean,
refShorthandDefaultPos: ?Pos,
refNeedsArrowPos: ?Pos,
refTrailingCommaPos?: Pos,
): ?N.Expression {
let elt;
if (allowEmpty && this.match(tt.comma)) {
@@ -1936,10 +1900,6 @@ export default class ExpressionParser extends LValParser {
spreadNodeStartPos,
spreadNodeStartLoc,
);
if (refTrailingCommaPos && this.match(tt.comma)) {
refTrailingCommaPos.start = this.state.start;
}
} else {
elt = this.parseMaybeAssign(
false,

View File

@@ -122,10 +122,7 @@ export default class LValParser extends NodeUtils {
this.raise(prop.key.start, error);
} else if (prop.type === "SpreadElement" && !isLast) {
this.raise(
prop.start,
"The rest element has to be the last element when destructuring",
);
this.raiseRestNotLast(prop.start, "property");
} else {
this.toAssignable(prop, isBinding, "object destructuring pattern");
}
@@ -162,13 +159,12 @@ export default class LValParser extends NodeUtils {
}
for (let i = 0; i < end; i++) {
const elt = exprList[i];
if (elt && elt.type === "SpreadElement") {
this.raise(
elt.start,
"The rest element has to be the last element when destructuring",
);
if (elt) {
this.toAssignable(elt, isBinding, contextDescription);
if (elt.type === "RestElement") {
this.raiseRestNotLast(elt.start, "element");
}
}
if (elt) this.toAssignable(elt, isBinding, contextDescription);
}
return exprList;
}
@@ -199,10 +195,10 @@ export default class LValParser extends NodeUtils {
// Parses spread element.
parseSpread<T: RestElement | SpreadElement>(
parseSpread(
refShorthandDefaultPos: ?Pos,
refNeedsArrowPos?: ?Pos,
): T {
): SpreadElement {
const node = this.startNode();
this.next();
node.argument = this.parseMaybeAssign(
@@ -211,6 +207,11 @@ export default class LValParser extends NodeUtils {
undefined,
refNeedsArrowPos,
);
if (this.state.commaAfterSpreadAt === -1 && this.match(tt.comma)) {
this.state.commaAfterSpreadAt = this.state.start;
}
return this.finishNode(node, "SpreadElement");
}
@@ -273,20 +274,13 @@ export default class LValParser extends NodeUtils {
break;
} else if (this.match(tt.ellipsis)) {
elts.push(this.parseAssignableListItemTypes(this.parseRest()));
if (
this.state.inFunction &&
this.state.inParameters &&
this.match(tt.comma)
) {
const nextTokenType = this.lookahead().type;
const errorMessage =
nextTokenType === tt.parenR
? "A trailing comma is not permitted after the rest element"
: "Rest parameter must be last formal parameter";
this.raise(this.state.start, errorMessage);
} else {
this.expect(close);
}
this.checkCommaAfterRest(
close,
this.state.inFunction && this.state.inParameters
? "parameter"
: "element",
);
this.expect(close);
break;
} else {
const decorators = [];
@@ -440,4 +434,28 @@ export default class LValParser extends NodeUtils {
this.raise(node.argument.start, "Invalid rest operator's argument");
}
checkCommaAfterRest(close: TokenType, kind: string): void {
if (this.match(tt.comma)) {
if (this.lookahead().type === close) {
this.raiseCommaAfterRest(this.state.start, kind);
} else {
this.raiseRestNotLast(this.state.start, kind);
}
}
}
checkCommaAfterRestFromSpread(kind: string): void {
if (this.state.commaAfterSpreadAt > -1) {
this.raiseCommaAfterRest(this.state.commaAfterSpreadAt, kind);
}
}
raiseCommaAfterRest(pos: number, kind: string) {
this.raise(pos, `A trailing comma is not permitted after the rest ${kind}`);
}
raiseRestNotLast(pos: number, kind: string) {
this.raise(pos, `The rest ${kind} must be the last ${kind}`);
}
}

View File

@@ -546,17 +546,10 @@ export default (superClass: Class<Parser>): Class<Parser> =>
// No mandatory elements may follow optional elements
// If there's a rest element, it must be at the end of the tuple
let seenOptionalElement = false;
node.elementTypes.forEach((elementNode, i) => {
if (elementNode.type === "TSRestType") {
if (i !== node.elementTypes.length - 1) {
this.raise(
elementNode.start,
"A rest element must be last in a tuple type.",
);
}
} else if (elementNode.type === "TSOptionalType") {
node.elementTypes.forEach(elementNode => {
if (elementNode.type === "TSOptionalType") {
seenOptionalElement = true;
} else if (seenOptionalElement) {
} else if (seenOptionalElement && elementNode.type !== "TSRestType") {
this.raise(
elementNode.start,
"A required element cannot follow an optional element.",
@@ -573,6 +566,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
const restNode: N.TsRestType = this.startNode();
this.next(); // skips ellipsis
restNode.typeAnnotation = this.tsParseType();
this.checkCommaAfterRest(tt.bracketR, "type");
return this.finishNode(restNode, "TSRestType");
}

View File

@@ -15,6 +15,8 @@ export default class State {
this.input = input;
this.commaAfterSpreadAt = -1;
this.potentialArrowAt = -1;
this.noArrowAt = [];
@@ -87,6 +89,11 @@ export default class State {
// TODO
input: string;
// A comma after "...a" is only allowed in spread, but not in rest.
// Since we parse destructuring patterns as array/object literals
// and then convert them, we need to track it.
commaAfterSpreadAt: number;
// Used to signify the start of a potential arrow function
potentialArrowAt: number;