Skip to content

Commit

Permalink
Add support for optional chain in assignments (#15751)
Browse files Browse the repository at this point in the history
Co-authored-by: CanadaHonk <19228318+CanadaHonk@users.noreply.github.com>
  • Loading branch information
nicolo-ribaudo and CanadaHonk committed Sep 25, 2023
1 parent 672b881 commit 4d93bae
Show file tree
Hide file tree
Showing 125 changed files with 1,513 additions and 80 deletions.
6 changes: 5 additions & 1 deletion packages/babel-helper-function-name/src/index.ts
Expand Up @@ -224,7 +224,11 @@ export default function <N extends t.FunctionExpression | t.Class>(
node: N;
parent?: NodePath<N>["parent"];
scope: Scope;
id?: t.LVal | t.StringLiteral | t.NumericLiteral | t.BigIntLiteral;
id?:
| t.AssignmentExpression["left"]
| t.StringLiteral
| t.NumericLiteral
| t.BigIntLiteral;
},
localBinding = false,
supportUnicodeId = false,
Expand Down
99 changes: 55 additions & 44 deletions packages/babel-helper-member-expression-to-functions/src/index.ts
Expand Up @@ -169,12 +169,12 @@ const handle = {
const willEndPathCastToBoolean = willPathCastToBoolean(endPath);

const rootParentPath = endPath.parentPath;
if (
rootParentPath.isUpdateExpression({ argument: node }) ||
rootParentPath.isAssignmentExpression({ left: node })
) {
throw member.buildCodeFrameError(`can't handle assignment`);
if (rootParentPath.isUpdateExpression({ argument: node })) {
throw member.buildCodeFrameError(`can't handle update expression`);
}
const isAssignment = rootParentPath.isAssignmentExpression({
left: endPath.node,
});
const isDeleteOperation = rootParentPath.isUnaryExpression({
operator: "delete",
});
Expand Down Expand Up @@ -252,6 +252,9 @@ const handle = {
parentPath.isUnaryExpression({ operator: "delete" })
) {
parentPath.replaceWith(this.delete(member));
} else if (parentPath.isAssignmentExpression()) {
// `a?.#b = c` to `(a == null ? void 0 : a.#b = c)`
handleAssignment(this, member, parentPath);
} else {
member.replaceWith(this.get(member));
}
Expand Down Expand Up @@ -295,7 +298,7 @@ const handle = {
}

let replacementPath: NodePath = endPath;
if (isDeleteOperation) {
if (isDeleteOperation || isAssignment) {
replacementPath = endParentPath;
regular = endParentPath.node;
}
Expand Down Expand Up @@ -432,44 +435,7 @@ const handle = {
// MEMBER += VALUE -> _set(MEMBER, _get(MEMBER) + VALUE)
// MEMBER ??= VALUE -> _get(MEMBER) ?? _set(MEMBER, VALUE)
if (parentPath.isAssignmentExpression({ left: node })) {
if (this.simpleSet) {
member.replaceWith(this.simpleSet(member));
return;
}

const { operator, right: value } = parentPath.node;

if (operator === "=") {
parentPath.replaceWith(this.set(member, value));
} else {
const operatorTrunc = operator.slice(0, -1);
if (LOGICAL_OPERATORS.includes(operatorTrunc)) {
// Give the state handler a chance to memoise the member, since we'll
// reference it twice. The first access (the get) should do the memo
// assignment.
this.memoise(member, 1);
parentPath.replaceWith(
logicalExpression(
operatorTrunc as t.LogicalExpression["operator"],
this.get(member),
this.set(member, value),
),
);
} else {
// Here, the second access (the set) is evaluated first.
this.memoise(member, 2);
parentPath.replaceWith(
this.set(
member,
binaryExpression(
operatorTrunc as t.BinaryExpression["operator"],
this.get(member),
value,
),
),
);
}
}
handleAssignment(this, member, parentPath);
return;
}

Expand Down Expand Up @@ -549,6 +515,51 @@ const handle = {
},
};

function handleAssignment(
state: HandlerState,
member: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
parentPath: NodePath<t.AssignmentExpression>,
) {
if (state.simpleSet) {
member.replaceWith(state.simpleSet(member));
return;
}

const { operator, right: value } = parentPath.node;

if (operator === "=") {
parentPath.replaceWith(state.set(member, value));
} else {
const operatorTrunc = operator.slice(0, -1);
if (LOGICAL_OPERATORS.includes(operatorTrunc)) {
// Give the state handler a chance to memoise the member, since we'll
// reference it twice. The first access (the get) should do the memo
// assignment.
state.memoise(member, 1);
parentPath.replaceWith(
logicalExpression(
operatorTrunc as t.LogicalExpression["operator"],
state.get(member),
state.set(member, value),
),
);
} else {
// Here, the second access (the set) is evaluated first.
state.memoise(member, 2);
parentPath.replaceWith(
state.set(
member,
binaryExpression(
operatorTrunc as t.BinaryExpression["operator"],
state.get(member),
value,
),
),
);
}
}
}

export interface Handler<State> {
memoise?(
this: HandlerState<State> & State,
Expand Down
6 changes: 6 additions & 0 deletions packages/babel-helpers/src/helpers.ts
Expand Up @@ -1884,3 +1884,9 @@ helpers.identity = helper("7.17.0")`
return x;
}
`;

helpers.nullishReceiverError = helper("7.22.6")`
export default function _nullishReceiverError(r) {
throw new TypeError("Cannot set property of null or undefined.");
}
`;
17 changes: 17 additions & 0 deletions packages/babel-parser/data/schema.json
Expand Up @@ -145,6 +145,23 @@
"additionalItems": false,
"type": "array"
},
{
"items": [
{
"type": "string",
"enum": ["optionalChainingAssign"]
},
{
"type": "object",
"properties": {
"version": {
"type": "string",
"enum": ["2023-07"]
}
}
}
]
},
{
"enum": [
"asyncDoExpressions",
Expand Down
4 changes: 4 additions & 0 deletions packages/babel-parser/src/parse-error/standard-errors.ts
Expand Up @@ -147,6 +147,10 @@ export default {
`Invalid left-hand side in ${toNodeDescription(ancestor)}.`,
InvalidLhsBinding: ({ ancestor }: { ancestor: LValAncestor }) =>
`Binding invalid left-hand side in ${toNodeDescription(ancestor)}.`,
InvalidLhsOptionalChaining: ({ ancestor }: { ancestor: LValAncestor }) =>
`Invalid optional chaining in the left-hand side of ${toNodeDescription(
ancestor,
)}.`,
InvalidNumber: "Invalid number.",
InvalidOrMissingExponent:
"Floating-point numbers require a valid exponent after the 'e'.",
Expand Down
29 changes: 24 additions & 5 deletions packages/babel-parser/src/parser/lval.ts
Expand Up @@ -113,7 +113,10 @@ export default abstract class LValParser extends NodeUtils {
Errors.InvalidParenthesizedAssignment,
{ at: node },
);
} else if (parenthesized.type !== "MemberExpression") {
} else if (
parenthesized.type !== "MemberExpression" &&
!this.isOptionalMemberExpression(parenthesized)
) {
// 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
// i.e. `([(a.b) = []] = []) => {}`
Expand Down Expand Up @@ -552,6 +555,11 @@ export default abstract class LValParser extends NodeUtils {
);
}

// Overridden by the estree plugin
isOptionalMemberExpression(expression: Node) {
return expression.type === "OptionalMemberExpression";
}

/**
* Verify that a target expression is an lval (something that can be assigned to).
*
Expand Down Expand Up @@ -600,7 +608,20 @@ export default abstract class LValParser extends NodeUtils {
// toAssignable already reported this error with a nicer message.
if (this.isObjectMethod(expression)) return;

if (type === "MemberExpression") {
const isOptionalMemberExpression =
this.isOptionalMemberExpression(expression);

if (isOptionalMemberExpression || type === "MemberExpression") {
if (isOptionalMemberExpression) {
this.expectPlugin("optionalChainingAssign", expression.loc.start);
if (ancestor.type !== "AssignmentExpression") {
this.raise(Errors.InvalidLhsOptionalChaining, {
at: expression,
ancestor,
});
}
}

if (binding !== BindingFlag.TYPE_NONE) {
this.raise(Errors.InvalidPropertyBindingPattern, { at: expression });
}
Expand Down Expand Up @@ -649,9 +670,7 @@ export default abstract class LValParser extends NodeUtils {
? validity
: [validity, type === "ParenthesizedExpression"];
const nextAncestor =
type === "ArrayPattern" ||
type === "ObjectPattern" ||
type === "ParenthesizedExpression"
type === "ArrayPattern" || type === "ObjectPattern"
? ({ type } as const)
: ancestor;

Expand Down
11 changes: 11 additions & 0 deletions packages/babel-parser/src/plugin-utils.ts
Expand Up @@ -232,6 +232,17 @@ export function validatePlugins(plugins: PluginList) {
error.missingPlugins = "doExpressions";
throw error;
}

if (
hasPlugin(plugins, "optionalChainingAssign") &&
getPluginOption(plugins, "optionalChainingAssign", "version") !== "2023-07"
) {
throw new Error(
"The 'optionalChainingAssign' plugin requires a 'version' option," +
" representing the last proposal update. Currently, the" +
" only supported value is '2023-07'.",
);
}
}

// These plugins are defined using a mixin which extends the parser class.
Expand Down
7 changes: 7 additions & 0 deletions packages/babel-parser/src/plugins/estree.ts
Expand Up @@ -535,6 +535,13 @@ export default (superClass: typeof Parser) =>
return node;
}

isOptionalMemberExpression(node: N.Node) {
if (node.type === "ChainExpression") {
return node.expression.type === "MemberExpression";
}
return super.isOptionalMemberExpression(node);
}

hasPropertyAsPrivateName(node: N.Node): boolean {
if (node.type === "ChainExpression") {
node = node.expression;
Expand Down
1 change: 1 addition & 0 deletions packages/babel-parser/src/typings.d.ts
Expand Up @@ -47,6 +47,7 @@ export type ParserPluginWithOptions =
| ["importAttributes", { deprecatedAssertSyntax: boolean }]
// @deprecated
| ["moduleAttributes", { version: "may-2020" }]
| ["optionalChainingAssign", { version: "2023-07" }]
| ["pipelineOperator", PipelineOperatorPluginOptions]
| ["recordAndTuple", RecordAndTuplePluginOptions]
| ["flow", FlowPluginOptions]
Expand Down
Expand Up @@ -2,7 +2,7 @@
"type": "File",
"start":0,"end":9,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":9,"index":9}},
"errors": [
"SyntaxError: Invalid left-hand side in parenthesized expression. (1:1)"
"SyntaxError: Invalid left-hand side in assignment expression. (1:1)"
],
"program": {
"type": "Program",
Expand Down
@@ -0,0 +1 @@
a?.b = c;
@@ -0,0 +1,3 @@
{
"throws": "This experimental syntax requires enabling the parser plugin: \"optionalChainingAssign\". (1:0)"
}
@@ -0,0 +1 @@
[a?.b] = [];
@@ -0,0 +1,52 @@
{
"type": "File",
"start":0,"end":12,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":12,"index":12}},
"errors": [
"SyntaxError: Invalid optional chaining in the left-hand side of array destructuring pattern. (1:1)"
],
"program": {
"type": "Program",
"start":0,"end":12,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":12,"index":12}},
"sourceType": "script",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start":0,"end":12,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":12,"index":12}},
"expression": {
"type": "AssignmentExpression",
"start":0,"end":11,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":11,"index":11}},
"operator": "=",
"left": {
"type": "ArrayPattern",
"start":0,"end":6,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":1,"column":6,"index":6}},
"elements": [
{
"type": "OptionalMemberExpression",
"start":1,"end":5,"loc":{"start":{"line":1,"column":1,"index":1},"end":{"line":1,"column":5,"index":5}},
"object": {
"type": "Identifier",
"start":1,"end":2,"loc":{"start":{"line":1,"column":1,"index":1},"end":{"line":1,"column":2,"index":2},"identifierName":"a"},
"name": "a"
},
"computed": false,
"property": {
"type": "Identifier",
"start":4,"end":5,"loc":{"start":{"line":1,"column":4,"index":4},"end":{"line":1,"column":5,"index":5},"identifierName":"b"},
"name": "b"
},
"optional": true
}
]
},
"right": {
"type": "ArrayExpression",
"start":9,"end":11,"loc":{"start":{"line":1,"column":9,"index":9},"end":{"line":1,"column":11,"index":11}},
"elements": []
}
}
}
],
"directives": []
}
}
@@ -0,0 +1 @@
({ prop: a?.b } = {});

0 comments on commit 4d93bae

Please sign in to comment.