From fd5a04927ee3fb04dbfefddc65dff20c5878f63d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 2 Jun 2025 10:00:05 +0200 Subject: [PATCH] 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 --- .../compiler/src/expression_parser/parser.ts | 7 ++++- .../test/expression_parser/parser_spec.ts | 31 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 1e8f4f7d35b..1decf52d0d9 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -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(); diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index 7d41b22534d..ec9857c2373 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -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) {