diff --git a/packages/compiler/src/expression_parser/parser.ts b/packages/compiler/src/expression_parser/parser.ts index 3d24447079a13..5b4c29948fc94 100644 --- a/packages/compiler/src/expression_parser/parser.ts +++ b/packages/compiler/src/expression_parser/parser.ts @@ -586,10 +586,18 @@ export class _ParseAST { while (this.consumeOptionalCharacter(chars.$SEMICOLON)) { } // read all semicolons } else if (this.index < this.tokens.length) { + const errorIndex = this.index; this.error(`Unexpected token '${this.next}'`); + // The `error` call above will skip ahead to the next recovery point in an attempt to + // recover part of the expression, but that might be the token we started from which will + // lead to an infinite loop. If that's the case, break the loop assuming that we can't + // parse further. + if (this.index === errorIndex) { + break; + } } } - if (exprs.length == 0) { + if (exprs.length === 0) { // We have no expressions so create an empty expression that spans the entire input length const artificialStart = this.offset; const artificialEnd = this.offset + this.input.length; diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts index e7e6c5fe79fd9..94556aec6cd66 100644 --- a/packages/compiler/test/expression_parser/parser_spec.ts +++ b/packages/compiler/test/expression_parser/parser_spec.ts @@ -321,6 +321,15 @@ describe('parser', () => { expect(ast.errors.length).toBe(1); expect(ast.errors[0].message).toContain('Unexpected token \'=\''); }); + + it('should recover on parenthesized empty rvalues', () => { + const ast = parseAction('(a[1] = b) = c = d'); + expect(unparse(ast)).toEqual('a[1] = b'); + validate(ast); + + expect(ast.errors.length).toBe(1); + expect(ast.errors[0].message).toContain('Unexpected token \'=\''); + }); }); });