refactor(compiler): produce binary expressions instead of dedicated write ones (#61682)

Currently our expression parser produces two different expressions for writes: `PropertyWrite` (e.g. `foo.bar = 123`) or `KeyedWrite` (e.g. `foo[0] = 123`). This is inconsistent with other ASTs, like TypeScript's, where writes are represented as binary expressions with a `=` operator and it makes it difficult to implement more write operators like `??=`, because we'd essentially have to duplicate them.

These changes switch the expression parser over to produce binary expressions instead.

PR Close #61682
This commit is contained in:
Kristiyan Kostadinov
2025-05-26 12:58:15 +02:00
committed by kirjs
parent c663277df6
commit 08ee693995
2 changed files with 15 additions and 15 deletions
@@ -29,7 +29,6 @@ import {
ImplicitReceiver,
Interpolation,
KeyedRead,
KeyedWrite,
LiteralArray,
LiteralMap,
LiteralMapKey,
@@ -40,7 +39,6 @@ import {
ParseSpan,
PrefixNot,
PropertyRead,
PropertyWrite,
RecursiveAstVisitor,
SafeCall,
SafeKeyedRead,
@@ -1214,14 +1212,13 @@ class _ParseAST {
return id;
});
const nameSpan = this.sourceSpan(nameStart);
let receiver: AST;
if (isSafe) {
if (this.consumeOptionalOperator('=')) {
this.error("The '?.' operator cannot be used in the assignment");
receiver = new EmptyExpr(this.span(start), this.sourceSpan(start));
return new EmptyExpr(this.span(start), this.sourceSpan(start));
} else {
receiver = new SafePropertyRead(
return new SafePropertyRead(
this.span(start),
this.sourceSpan(start),
nameSpan,
@@ -1235,18 +1232,17 @@ class _ParseAST {
this.error('Bindings cannot contain assignments');
return new EmptyExpr(this.span(start), this.sourceSpan(start));
}
const value = this.parseConditional();
receiver = new PropertyWrite(
const receiver = new PropertyRead(
this.span(start),
this.sourceSpan(start),
nameSpan,
readReceiver,
id,
value,
);
const value = this.parseConditional();
return new Binary(this.span(start), this.sourceSpan(start), '=', receiver, value);
} else {
receiver = new PropertyRead(
return new PropertyRead(
this.span(start),
this.sourceSpan(start),
nameSpan,
@@ -1255,8 +1251,6 @@ class _ParseAST {
);
}
}
return receiver;
}
private parseCall(receiver: AST, start: number, isSafe: boolean): AST {
@@ -1375,8 +1369,14 @@ class _ParseAST {
if (isSafe) {
this.error("The '?.' operator cannot be used in the assignment");
} else {
const binaryReceiver = new KeyedRead(
this.span(start),
this.sourceSpan(start),
receiver,
key,
);
const value = this.parseConditional();
return new KeyedWrite(this.span(start), this.sourceSpan(start), receiver, key, value);
return new Binary(this.span(start), this.sourceSpan(start), '=', binaryReceiver, value);
}
} else {
return isSafe
@@ -532,13 +532,13 @@ describe('parser', () => {
it('should record property write span', () => {
const ast = parseAction('a = b');
expect(unparseWithSpan(ast)).toContain(['a = b', 'a = b']);
expect(unparseWithSpan(ast)).toContain(['a = b', '[nameSpan] a']);
expect(unparseWithSpan(ast)).toContain(['a', '[nameSpan] a']);
});
it('should record accessed property write span', () => {
const ast = parseAction('a.b = c');
expect(unparseWithSpan(ast)).toContain(['a.b = c', 'a.b = c']);
expect(unparseWithSpan(ast)).toContain(['a.b = c', '[nameSpan] b']);
expect(unparseWithSpan(ast)).toContain(['a.b', '[nameSpan] b']);
});
it('should record spans for untagged template literals with no interpolations', () => {