fix(compiler): recover invalid parenthesized expressions (#61815)

When the expression parser consumes tokens inside a parenthesized expression, it looks for valid tokens until it hits and invalid one or a closing paren. If it finds an invalid token, it reports and error and tries to recover until it finds a closing paren. The problem is that in such cases, it would produce the `ParenthesizedExpression` and continue parsing **from** from the closing paren which would then produce more errors that add noise to the output and result in an incorrect representation of the user's code. E.g. `foo((event.target as HTMLElement).value)` would be recovered to `foo((event.target)).value` instead of `foo((event.target).value)`.

These changes resolve the issue by skipping over the closing paren at the recovery point.

Fixes #61792.

PR Close #61815
This commit is contained in:
Kristiyan Kostadinov 2025-06-02 10:00:05 +02:00 committed by kirjs
parent 07cb321f00
commit fd5a04927e
2 changed files with 34 additions and 4 deletions

View file

@ -1082,8 +1082,13 @@ class _ParseAST {
if (this.consumeOptionalCharacter(chars.$LPAREN)) {
this.rparensExpected++;
const result = this.parsePipe();
if (!this.consumeOptionalCharacter(chars.$RPAREN)) {
this.error('Missing closing parentheses');
// Calling into `error` above will attempt to recover up until the next closing paren.
// If that's the case, consume it so we can partially recover the expression.
this.consumeOptionalCharacter(chars.$RPAREN);
}
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
return new ParenthesizedExpression(this.span(start), this.sourceSpan(start), result);
} else if (this.next.isKeywordNull()) {
this.advance();

View file

@ -658,6 +658,19 @@ describe('parser', () => {
it('should report a missing expected token', () => {
expectActionError('a(b', 'Missing expected ) at the end of the expression [a(b]');
});
it('should report a single error for an `as` expression inside a parenthesized expression', () => {
expectActionError(
`foo(($event.target as HTMLElement).value)`,
'Missing closing parentheses at column 20',
1,
);
expectActionError(
`foo(((($event.target as HTMLElement))).value)`,
'Missing closing parentheses at column 22',
1,
);
});
});
describe('parseBinding', () => {
@ -1373,6 +1386,12 @@ describe('parser', () => {
it('should be able to recover from a missing selector', () => recover('a.'));
it('should be able to recover from a missing selector in a array literal', () =>
recover('[[a.], b, c]'));
it('should recover from parenthesized `as` expressions', () => {
recover('foo(($event.target as HTMLElement).value)', 'foo(($event.target).value)');
recover('foo(((($event.target as HTMLElement))).value)', 'foo(((($event.target))).value)');
recover('foo(((bar as HTMLElement) as Something).value)', 'foo(((bar)).value)');
});
});
describe('offsets', () => {
@ -1468,7 +1487,13 @@ function checkAction(exp: string, expected?: string) {
validate(ast);
}
function expectError(ast: {errors: ParserError[]}, message: string) {
function expectError(ast: {errors: ParserError[]}, message: string, errorCount?: number) {
if (errorCount != null) {
expect(ast.errors.length).toBe(errorCount);
} else {
expect(ast.errors.length).toBeGreaterThan(0);
}
for (const error of ast.errors) {
if (error.message.indexOf(message) >= 0) {
return;
@ -1480,8 +1505,8 @@ function expectError(ast: {errors: ParserError[]}, message: string) {
);
}
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
function expectActionError(text: string, message: string, errorCount?: number) {
expectError(validate(parseAction(text)), message, errorCount);
}
function expectBindingError(text: string, message: string) {