From 92ebfd1ca75a7949062ea6c42f00a457739d4247 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Sun, 23 Jul 2023 10:08:11 +0300 Subject: [PATCH] refactor(compiler): handle braces in block parameters (#51143) Fixes that using braces in the block parameters would result in incorrect tokens being produced. Currently we don't have any blocks that allow object literal parameters, but it may come up in the future. PR Close #51143 --- packages/compiler/src/ml_parser/lexer.ts | 12 +++++-- .../compiler/test/ml_parser/lexer_spec.ts | 36 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index ce170b11961..d5d3b670271 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/packages/compiler/src/ml_parser/lexer.ts @@ -263,11 +263,11 @@ class _Tokenizer { this._beginToken(TokenType.BLOCK_PARAMETER); const start = this._cursor.clone(); let inQuote: number|null = null; + let openBraces = 0; // Consume the parameter until the next semicolon or brace. // Note that we skip over semicolons/braces inside of strings. - while ((this._cursor.peek() !== chars.$SEMICOLON && this._cursor.peek() !== chars.$RBRACE && - this._cursor.peek() !== chars.$EOF) || + while ((this._cursor.peek() !== chars.$SEMICOLON && this._cursor.peek() !== chars.$EOF) || inQuote !== null) { const char = this._cursor.peek(); @@ -278,6 +278,14 @@ class _Tokenizer { inQuote = null; } else if (inQuote === null && chars.isQuote(char)) { inQuote = char; + } else if (char === chars.$LBRACE && inQuote === null) { + openBraces++; + } else if (char === chars.$RBRACE && inQuote === null) { + if (openBraces === 0) { + break; + } else if (openBraces > 0) { + openBraces--; + } } this._cursor.advance(); diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index ea5a1a3c548..f039c907638 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -1817,8 +1817,10 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u describe('blocks', () => { // TODO(crisbeto): temporary utility while blocks are disabled by default. - function tokenizeBlock(input: string, options: TokenizeOptions = {}): any[] { - return tokenizeAndHumanizeParts(input, {tokenizeBlocks: true, ...options}); + const options: Readonly = {tokenizeBlocks: true}; + + function tokenizeBlock(input: string, additionalOptions: TokenizeOptions = {}): any[] { + return tokenizeAndHumanizeParts(input, {...options, ...additionalOptions}); } it('should parse a block group', () => { @@ -1887,10 +1889,11 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u }); it('should handle semicolons and braces used in a block group parameter', () => { - expect(tokenizeBlock(`{#foo a === ";"; b === '}'}hello{/foo}`)).toEqual([ + expect(tokenizeBlock(`{#foo a === ";"; b === '}'; c === "{"}hello{/foo}`)).toEqual([ [TokenType.BLOCK_GROUP_OPEN_START, 'foo'], [TokenType.BLOCK_PARAMETER, `a === ";"`], [TokenType.BLOCK_PARAMETER, `b === '}'`], + [TokenType.BLOCK_PARAMETER, `c === "{"`], [TokenType.BLOCK_GROUP_OPEN_END], [TokenType.TEXT, 'hello'], [TokenType.BLOCK_GROUP_CLOSE, 'foo'], @@ -1898,9 +1901,20 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u ]); }); - it('should throw for invalid quotes in a parameter', () => { - const options = {tokenizeBlocks: true}; + it('should handle object literals in block group parameters', () => { + expect(tokenizeBlock(`{#foo on a({a: 1, b: 2}, false, {c: 3}); when b({d: 4})}hello{/foo}`)) + .toEqual([ + [TokenType.BLOCK_GROUP_OPEN_START, 'foo'], + [TokenType.BLOCK_PARAMETER, 'on a({a: 1, b: 2}, false, {c: 3})'], + [TokenType.BLOCK_PARAMETER, 'when b({d: 4})'], + [TokenType.BLOCK_GROUP_OPEN_END], + [TokenType.TEXT, 'hello'], + [TokenType.BLOCK_GROUP_CLOSE, 'foo'], + [TokenType.EOF], + ]); + }); + it('should report invalid quotes in a parameter', () => { expect(tokenizeAndHumanizeErrors(`{#foo a === "}hello{/foo}`, options)).toEqual([ [TokenType.BLOCK_PARAMETER, 'Unexpected character "EOF"', '0:25'] ]); @@ -1910,6 +1924,18 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_u ]); }); + it('should report unclosed block open tag containing an objet literal', () => { + expect(tokenizeAndHumanizeErrors(`{#foo {invalid: true}hello{/foo}`, options)).toEqual([ + [TokenType.BLOCK_GROUP_OPEN_END, 'Unexpected character "EOF"', '0:32'], + ]); + }); + + it('should report unclosed object literal in block open tag', () => { + expect(tokenizeAndHumanizeErrors(`{#foo {hello{/foo}`, options)).toEqual([ + [TokenType.BLOCK_GROUP_OPEN_END, 'Unexpected character "EOF"', '0:18'], + ]); + }); + it('should handle a semicolon used in a nested string inside a block group parameter', () => { expect(tokenizeBlock(`{#if condition === "';'"}hello{/if}`)).toEqual([ [TokenType.BLOCK_GROUP_OPEN_START, 'if'],