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

Add support for optional chain in assignments #15751

Merged
merged 17 commits into from Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

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

All this section is moved to the handleAssignment function below with no changes (other than renaming this to state).

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 } = {});