Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing ts type casts and nested patterns in destructuring #14500

Merged
merged 14 commits into from May 1, 2022
6 changes: 4 additions & 2 deletions packages/babel-parser/src/parser/expression.js
Expand Up @@ -303,7 +303,8 @@ export default class ExpressionParser extends LValParser {
node.operator = operator;

if (this.match(tt.eq)) {
node.left = this.toAssignable(left, /* isLHS */ true);
this.toAssignable(left, /* isLHS */ true);
node.left = left;

if (
refExpressionErrors.doubleProtoLoc != null &&
Expand Down Expand Up @@ -2425,7 +2426,8 @@ export default class ExpressionParser extends LValParser {
params: N.Expression[],
trailingCommaLoc: ?Position,
): void {
node.params = this.toAssignableList(params, trailingCommaLoc, false);
this.toAssignableList(params, trailingCommaLoc, false);
node.params = params;
}

parseFunctionBodyAndFinish(
Expand Down
124 changes: 65 additions & 59 deletions packages/babel-parser/src/parser/lval.js
Expand Up @@ -90,23 +90,25 @@ export default class LValParser extends NodeUtils {
When this one is updated, please check if also that one needs to be updated.

* @param {Node} node The expression atom
* @param {boolean} [isLHS=false] Whether we are parsing a LeftHandSideExpression. If isLHS is `true`, the following cases are allowed:
`[(a)] = [0]`, `[(a.b)] = [0]`

* @returns {Node} The converted assignable pattern
* @param {boolean} [isLHS=false] Whether we are parsing a LeftHandSideExpression.
* If isLHS is `true`, the following cases are allowed: `[(a)] = [0]`, `[(a.b)] = [0]`
* If isLHS is `false`, we are in an arrow function parameters list.
* @memberof LValParser
*/
toAssignable(node: Node, isLHS: boolean = false): Node {
toAssignable(node: Node, isLHS: boolean = false): void {
let parenthesized = undefined;
if (node.type === "ParenthesizedExpression" || node.extra?.parenthesized) {
parenthesized = unwrapParenthesizedExpression(node);
if (isLHS) {
// an LHS can be reinterpreted to a binding pattern but not vice versa.
// therefore a parenthesized identifier is ambiguous until we are sure it is an assignment expression
// i.e. `([(a) = []] = []) => {}`
// see also `recordParenthesizedIdentifierError` signature in packages/babel-parser/src/util/expression-scope.js
// see also `recordArrowParemeterBindingError` signature in packages/babel-parser/src/util/expression-scope.js
if (parenthesized.type === "Identifier") {
this.expressionScope.recordParenthesizedIdentifierError({ at: node });
this.expressionScope.recordArrowParemeterBindingError(
Errors.InvalidParenthesizedAssignment,
{ at: node },
);
} else if (parenthesized.type !== "MemberExpression") {
// A parenthesized member expression can be in LHS but not in pattern.
// If the LHS is later interpreted as a pattern, `checkLVal` will throw for member expression binding
Expand Down Expand Up @@ -162,12 +164,10 @@ export default class LValParser extends NodeUtils {
}

case "SpreadElement": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can handle SpreadElement in toAssignableObjectExpressionProp and toAssignableList, so we don't need to track isInObjectPattern anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! 69719e4

this.checkToRestConversion(node);

node.type = "RestElement";
const arg = node.argument;
this.toAssignable(arg, isLHS);
break;
throw new Error(
"Internal @babel/parser error (this is a bug, please report it)." +
" SpreadElement should be converted by .toAssignable's caller.",
);
}

case "ArrayExpression":
Expand Down Expand Up @@ -198,7 +198,6 @@ export default class LValParser extends NodeUtils {
// We don't know how to deal with this node. It will
// be reported by a later call to checkLVal
}
return node;
}

toAssignableObjectExpressionProp(
Expand All @@ -213,8 +212,15 @@ export default class LValParser extends NodeUtils {
: Errors.PatternHasMethod,
{ at: prop.key },
);
} else if (prop.type === "SpreadElement" && !isLast) {
this.raise(Errors.RestTrailingComma, { at: prop });
} else if (prop.type === "SpreadElement") {
prop.type = "RestElement";
const arg = prop.argument;
this.checkToRestConversion(arg, /* allowPattern */ false);
this.toAssignable(arg, isLHS);

if (!isLast) {
this.raise(Errors.RestTrailingComma, { at: prop });
}
} else {
this.toAssignable(prop, isLHS);
}
Expand All @@ -226,43 +232,30 @@ export default class LValParser extends NodeUtils {
exprList: Expression[],
trailingCommaLoc?: ?Position,
isLHS: boolean,
): $ReadOnlyArray<Pattern> {
let end = exprList.length;
if (end) {
const last = exprList[end - 1];
if (last?.type === "RestElement") {
--end;
} else if (last?.type === "SpreadElement") {
last.type = "RestElement";
let arg = last.argument;
this.toAssignable(arg, isLHS);
arg = unwrapParenthesizedExpression(arg);
if (
arg.type !== "Identifier" &&
arg.type !== "MemberExpression" &&
arg.type !== "ArrayPattern" &&
arg.type !== "ObjectPattern"
) {
this.unexpected(arg.start);
}

if (trailingCommaLoc) {
this.raise(Errors.RestTrailingComma, { at: trailingCommaLoc });
}
): void {
const end = exprList.length - 1;

--end;
}
}
for (let i = 0; i < end; i++) {
for (let i = 0; i <= end; i++) {
const elt = exprList[i];
if (elt) {
if (!elt) continue;

if (elt.type === "SpreadElement") {
elt.type = "RestElement";
const arg = elt.argument;
this.checkToRestConversion(arg, /* allowPattern */ true);
this.toAssignable(arg, isLHS);
} else {
this.toAssignable(elt, isLHS);
if (elt.type === "RestElement") {
}

if (elt.type === "RestElement") {
if (i < end) {
this.raise(Errors.RestTrailingComma, { at: elt });
} else if (trailingCommaLoc) {
this.raise(Errors.RestTrailingComma, { at: trailingCommaLoc });
}
}
}
return exprList;
}

isAssignable(node: Node, isBinding?: boolean): boolean {
Expand Down Expand Up @@ -513,8 +506,9 @@ export default class LValParser extends NodeUtils {
* `[key: string, parenthesized: false]`.
*
* @param {NodeType} type A Node `type` string
* @param {boolean} isParenthesized
* Whether the node in question is parenthesized.
* @param {boolean} isUnparenthesizedInAssign
* Whether the node in question is unparenthesized and its parent
* is either an assignment pattern or an assignment expression.
* @param {BindingTypes} binding
* The binding operation that is being considered for this potential
* LVal.
Expand All @@ -525,8 +519,13 @@ export default class LValParser extends NodeUtils {
* A `[string, boolean]` tuple if we need to check this child and
* treat is as parenthesized.
*/
// eslint-disable-next-line no-unused-vars
isValidLVal(type: string, isParenthesized: boolean, binding: BindingTypes) {
isValidLVal(
type: string,
// eslint-disable-next-line no-unused-vars
isUnparenthesizedInAssign: boolean,
// eslint-disable-next-line no-unused-vars
binding: BindingTypes,
) {
return getOwn(
{
AssignmentPattern: "left",
Expand Down Expand Up @@ -625,7 +624,8 @@ export default class LValParser extends NodeUtils {

const validity = this.isValidLVal(
expression.type,
hasParenthesizedAncestor || expression.extra?.parenthesized,
!(hasParenthesizedAncestor || expression.extra?.parenthesized) &&
ancestor.type === "AssignmentExpression",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was needed to disallow a as b = c, however we must now make sure to not disallow [a as b] = c.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a new test case?

for (a as b of []);

It is disallowed in 7.16.2 but tsc allows it, and it seems this PR also fixes this one, too.

binding,
);

Expand Down Expand Up @@ -707,14 +707,20 @@ export default class LValParser extends NodeUtils {
this.scope.declareName(identifier.name, binding, identifier.loc.start);
}

checkToRestConversion(node: SpreadElement): void {
if (
node.argument.type !== "Identifier" &&
node.argument.type !== "MemberExpression"
) {
this.raise(Errors.InvalidRestAssignmentPattern, {
at: node.argument,
});
checkToRestConversion(node: Node, allowPattern: boolean): void {
switch (node.type) {
case "ParenthesizedExpression":
this.checkToRestConversion(node.expression, allowPattern);
break;
case "Identifier":
case "MemberExpression":
break;
case "ArrayExpression":
case "ObjectExpression":
if (allowPattern) break;
/* falls through */
default:
this.raise(Errors.InvalidRestAssignmentPattern, { at: node });
}
}

Expand Down
11 changes: 5 additions & 6 deletions packages/babel-parser/src/plugins/estree.js
Expand Up @@ -354,7 +354,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return super.isAssignable(node, isBinding);
}

toAssignable(node: N.Node, isLHS: boolean = false): N.Node {
toAssignable(node: N.Node, isLHS: boolean = false): void {
if (node != null && this.isObjectProperty(node)) {
const { key, value } = node;
if (this.isPrivateName(key)) {
Expand All @@ -364,19 +364,18 @@ export default (superClass: Class<Parser>): Class<Parser> =>
);
}
this.toAssignable(value, isLHS);
return node;
} else {
super.toAssignable(node, isLHS);
}

return super.toAssignable(node, isLHS);
}

toAssignableObjectExpressionProp(prop: N.Node, ...args) {
toAssignableObjectExpressionProp(prop: N.Node) {
if (prop.kind === "get" || prop.kind === "set") {
this.raise(Errors.PatternHasAccessor, { at: prop.key });
} else if (prop.method) {
this.raise(Errors.PatternHasMethod, { at: prop.key });
} else {
super.toAssignableObjectExpressionProp(prop, ...args);
super.toAssignableObjectExpressionProp(...arguments);
}
}

Expand Down
17 changes: 10 additions & 7 deletions packages/babel-parser/src/plugins/flow/index.js
Expand Up @@ -2362,27 +2362,30 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
}

toAssignable(node: N.Node, isLHS: boolean = false): N.Node {
if (node.type === "TypeCastExpression") {
return super.toAssignable(this.typeCastToParameter(node), isLHS);
} else {
return super.toAssignable(node, isLHS);
toAssignable(node: N.Node, isLHS: boolean = false): void {
if (
!isLHS &&
node.type === "AssignmentExpression" &&
node.left.type === "TypeCastExpression"
) {
node.left = this.typeCastToParameter(node.left);
}
super.toAssignable(...arguments);
}

// turn type casts that we found in function parameter head into type annotated params
toAssignableList(
exprList: N.Expression[],
trailingCommaLoc?: ?Position,
isLHS: boolean,
): $ReadOnlyArray<N.Pattern> {
): void {
for (let i = 0; i < exprList.length; i++) {
const expr = exprList[i];
if (expr?.type === "TypeCastExpression") {
exprList[i] = this.typeCastToParameter(expr);
}
}
return super.toAssignableList(exprList, trailingCommaLoc, isLHS);
super.toAssignableList(exprList, trailingCommaLoc, isLHS);
}

// this is a list of nodes, from something like a call expression, we need to filter the
Expand Down
6 changes: 3 additions & 3 deletions packages/babel-parser/src/plugins/placeholders.js
Expand Up @@ -140,16 +140,16 @@ export default (superClass: Class<Parser>): Class<Parser> =>
return type === "Placeholder" || super.isValidLVal(type, ...rest);
}

toAssignable(node: N.Node): N.Node {
toAssignable(node: N.Node): void {
if (
node &&
node.type === "Placeholder" &&
node.expectedNode === "Expression"
) {
node.expectedNode = "Pattern";
return node;
} else {
super.toAssignable(...arguments);
}
return super.toAssignable(...arguments);
}

/* ============================================================ *
Expand Down