/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {Lexer, StringTokenKind, Token} from '../../src/expression_parser/lexer'; function lex(text: string): any[] { return new Lexer().tokenize(text); } function expectToken(token: any, index: number, end: number) { expect(token instanceof Token).toBe(true); expect(token.index).toEqual(index); expect(token.end).toEqual(end); } function expectCharacterToken(token: any, index: number, end: number, character: string) { expect(character.length).toBe(1); expectToken(token, index, end); expect(token.isCharacter(character.charCodeAt(0))).toBe(true); } function expectOperatorToken(token: any, index: number, end: number, operator: string) { expectToken(token, index, end); expect(token.isOperator(operator)).toBe(true); } function expectNumberToken(token: any, index: number, end: number, n: number) { expectToken(token, index, end); expect(token.isNumber()).toBe(true); expect(token.toNumber()).toEqual(n); } function expectStringToken( token: any, index: number, end: number, str: string, kind: StringTokenKind, ) { expectToken(token, index, end); expect(token.isString()).toBe(true); expect(token.kind).toBe(kind); expect(token.toString()).toEqual(str); } function expectIdentifierToken(token: any, index: number, end: number, identifier: string) { expectToken(token, index, end); expect(token.isIdentifier()).toBe(true); expect(token.toString()).toEqual(identifier); } function expectPrivateIdentifierToken(token: any, index: number, end: number, identifier: string) { expectToken(token, index, end); expect(token.isPrivateIdentifier()).toBe(true); expect(token.toString()).toEqual(identifier); } function expectKeywordToken(token: any, index: number, end: number, keyword: string) { expectToken(token, index, end); expect(token.isKeyword()).toBe(true); expect(token.toString()).toEqual(keyword); } function expectErrorToken(token: Token, index: any, end: number, message: string) { expectToken(token, index, end); expect(token.isError()).toBe(true); expect(token.toString()).toEqual(message); } function expectRegExpBodyToken(token: any, index: number, end: number, str: string) { expectToken(token, index, end); expect(token.isRegExpBody()).toBe(true); expect(token.toString()).toEqual(str); } function expectRegExpFlagsToken(token: any, index: number, end: number, str: string) { expectToken(token, index, end); expect(token.isRegExpFlags()).toBe(true); expect(token.toString()).toEqual(str); } describe('lexer', () => { describe('token', () => { it('should tokenize a simple identifier', () => { const tokens: number[] = lex('j'); expect(tokens.length).toEqual(1); expectIdentifierToken(tokens[0], 0, 1, 'j'); }); it('should tokenize "this"', () => { const tokens: number[] = lex('this'); expect(tokens.length).toEqual(1); expectKeywordToken(tokens[0], 0, 4, 'this'); }); it('should tokenize a dotted identifier', () => { const tokens: number[] = lex('j.k'); expect(tokens.length).toEqual(3); expectIdentifierToken(tokens[0], 0, 1, 'j'); expectCharacterToken(tokens[1], 1, 2, '.'); expectIdentifierToken(tokens[2], 2, 3, 'k'); }); it('should tokenize a private identifier', () => { const tokens: number[] = lex('#a'); expect(tokens.length).toEqual(1); expectPrivateIdentifierToken(tokens[0], 0, 2, '#a'); }); it('should tokenize a property access with private identifier', () => { const tokens: number[] = lex('j.#k'); expect(tokens.length).toEqual(3); expectIdentifierToken(tokens[0], 0, 1, 'j'); expectCharacterToken(tokens[1], 1, 2, '.'); expectPrivateIdentifierToken(tokens[2], 2, 4, '#k'); }); it( 'should throw an invalid character error when a hash character is discovered but ' + 'not indicating a private identifier', () => { expectErrorToken( lex('#')[0], 0, 1, `Lexer Error: Invalid character [#] at column 0 in expression [#]`, ); expectErrorToken( lex('#0')[0], 0, 1, `Lexer Error: Invalid character [#] at column 0 in expression [#0]`, ); }, ); it('should tokenize an operator', () => { const tokens: number[] = lex('j-k'); expect(tokens.length).toEqual(3); expectOperatorToken(tokens[1], 1, 2, '-'); }); it('should tokenize an indexed operator', () => { const tokens: number[] = lex('j[k]'); expect(tokens.length).toEqual(4); expectCharacterToken(tokens[1], 1, 2, '['); expectCharacterToken(tokens[3], 3, 4, ']'); }); it('should tokenize a safe indexed operator', () => { const tokens: number[] = lex('j?.[k]'); expect(tokens.length).toBe(5); expectOperatorToken(tokens[1], 1, 3, '?.'); expectCharacterToken(tokens[2], 3, 4, '['); expectCharacterToken(tokens[4], 5, 6, ']'); }); it('should tokenize numbers', () => { const tokens: number[] = lex('88'); expect(tokens.length).toEqual(1); expectNumberToken(tokens[0], 0, 2, 88); }); it('should tokenize numbers within index ops', () => { expectNumberToken(lex('a[22]')[2], 2, 4, 22); }); it('should tokenize simple quoted strings', () => { expectStringToken(lex('"a"')[0], 0, 3, 'a', StringTokenKind.Plain); }); it('should tokenize quoted strings with escaped quotes', () => { expectStringToken(lex('"a\\""')[0], 0, 5, 'a"', StringTokenKind.Plain); }); it('should tokenize a string', () => { const tokens: Token[] = lex('j-a.bc[22]+1.3|f:\'a\\\'c\':"d\\"e"'); expectIdentifierToken(tokens[0], 0, 1, 'j'); expectOperatorToken(tokens[1], 1, 2, '-'); expectIdentifierToken(tokens[2], 2, 3, 'a'); expectCharacterToken(tokens[3], 3, 4, '.'); expectIdentifierToken(tokens[4], 4, 6, 'bc'); expectCharacterToken(tokens[5], 6, 7, '['); expectNumberToken(tokens[6], 7, 9, 22); expectCharacterToken(tokens[7], 9, 10, ']'); expectOperatorToken(tokens[8], 10, 11, '+'); expectNumberToken(tokens[9], 11, 14, 1.3); expectOperatorToken(tokens[10], 14, 15, '|'); expectIdentifierToken(tokens[11], 15, 16, 'f'); expectCharacterToken(tokens[12], 16, 17, ':'); expectStringToken(tokens[13], 17, 23, "a'c", StringTokenKind.Plain); expectCharacterToken(tokens[14], 23, 24, ':'); expectStringToken(tokens[15], 24, 30, 'd"e', StringTokenKind.Plain); }); it('should tokenize undefined', () => { const tokens: Token[] = lex('undefined'); expectKeywordToken(tokens[0], 0, 9, 'undefined'); expect(tokens[0].isKeywordUndefined()).toBe(true); }); it('should tokenize typeof', () => { const tokens: Token[] = lex('typeof'); expectKeywordToken(tokens[0], 0, 6, 'typeof'); expect(tokens[0].isKeywordTypeof()).toBe(true); }); it('should tokenize void', () => { const tokens: Token[] = lex('void'); expectKeywordToken(tokens[0], 0, 4, 'void'); expect(tokens[0].isKeywordVoid()).toBe(true); }); it('should tokenize in keyword', () => { const tokens: Token[] = lex('in'); expectKeywordToken(tokens[0], 0, 2, 'in'); expect(tokens[0].isKeywordIn()).toBe(true); }); it('should tokenize instanceof keyword', () => { const tokens: Token[] = lex('instanceof'); expectKeywordToken(tokens[0], 0, 10, 'instanceof'); expect(tokens[0].isKeywordInstanceOf()).toBe(true); }); it('should ignore whitespace', () => { const tokens: Token[] = lex('a \t \n \r b'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectIdentifierToken(tokens[1], 8, 9, 'b'); }); it('should tokenize quoted string', () => { const str = '[\'\\\'\', "\\""]'; const tokens: Token[] = lex(str); expectStringToken(tokens[1], 1, 5, "'", StringTokenKind.Plain); expectStringToken(tokens[3], 7, 11, '"', StringTokenKind.Plain); }); it('should tokenize escaped quoted string', () => { const str = '"\\"\\n\\f\\r\\t\\v\\u00A0"'; const tokens: Token[] = lex(str); expect(tokens.length).toEqual(1); expect(tokens[0].toString()).toEqual('"\n\f\r\t\v\u00A0'); }); it('should tokenize unicode', () => { const tokens: Token[] = lex('"\\u00A0"'); expect(tokens.length).toEqual(1); expect(tokens[0].toString()).toEqual('\u00a0'); }); it('should tokenize relation', () => { const tokens: Token[] = lex('! == != < > <= >= === !=='); expectOperatorToken(tokens[0], 0, 1, '!'); expectOperatorToken(tokens[1], 2, 4, '=='); expectOperatorToken(tokens[2], 5, 7, '!='); expectOperatorToken(tokens[3], 8, 9, '<'); expectOperatorToken(tokens[4], 10, 11, '>'); expectOperatorToken(tokens[5], 12, 14, '<='); expectOperatorToken(tokens[6], 15, 17, '>='); expectOperatorToken(tokens[7], 18, 21, '==='); expectOperatorToken(tokens[8], 22, 25, '!=='); }); it('should tokenize statements', () => { const tokens: Token[] = lex('a;b;'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectCharacterToken(tokens[1], 1, 2, ';'); expectIdentifierToken(tokens[2], 2, 3, 'b'); expectCharacterToken(tokens[3], 3, 4, ';'); }); it('should tokenize function invocation', () => { const tokens: Token[] = lex('a()'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectCharacterToken(tokens[1], 1, 2, '('); expectCharacterToken(tokens[2], 2, 3, ')'); }); it('should tokenize simple method invocations', () => { const tokens: Token[] = lex('a.method()'); expectIdentifierToken(tokens[2], 2, 8, 'method'); }); it('should tokenize method invocation', () => { const tokens: Token[] = lex('a.b.c (d) - e.f()'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectCharacterToken(tokens[1], 1, 2, '.'); expectIdentifierToken(tokens[2], 2, 3, 'b'); expectCharacterToken(tokens[3], 3, 4, '.'); expectIdentifierToken(tokens[4], 4, 5, 'c'); expectCharacterToken(tokens[5], 6, 7, '('); expectIdentifierToken(tokens[6], 7, 8, 'd'); expectCharacterToken(tokens[7], 8, 9, ')'); expectOperatorToken(tokens[8], 10, 11, '-'); expectIdentifierToken(tokens[9], 12, 13, 'e'); expectCharacterToken(tokens[10], 13, 14, '.'); expectIdentifierToken(tokens[11], 14, 15, 'f'); expectCharacterToken(tokens[12], 15, 16, '('); expectCharacterToken(tokens[13], 16, 17, ')'); }); it('should tokenize safe function invocation', () => { const tokens: Token[] = lex('a?.()'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectOperatorToken(tokens[1], 1, 3, '?.'); expectCharacterToken(tokens[2], 3, 4, '('); expectCharacterToken(tokens[3], 4, 5, ')'); }); it('should tokenize a safe method invocations', () => { const tokens: Token[] = lex('a.method?.()'); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectCharacterToken(tokens[1], 1, 2, '.'); expectIdentifierToken(tokens[2], 2, 8, 'method'); expectOperatorToken(tokens[3], 8, 10, '?.'); expectCharacterToken(tokens[4], 10, 11, '('); expectCharacterToken(tokens[5], 11, 12, ')'); }); it('should tokenize number', () => { expectNumberToken(lex('0.5')[0], 0, 3, 0.5); }); it('should tokenize multiplication and exponentiation', () => { const tokens: Token[] = lex('1 * 2 ** 3'); expectNumberToken(tokens[0], 0, 1, 1); expectOperatorToken(tokens[1], 2, 3, '*'); expectNumberToken(tokens[2], 4, 5, 2); expectOperatorToken(tokens[3], 6, 8, '**'); expectNumberToken(tokens[4], 9, 10, 3); }); it('should tokenize number with exponent', () => { let tokens: Token[] = lex('0.5E-10'); expect(tokens.length).toEqual(1); expectNumberToken(tokens[0], 0, 7, 0.5e-10); tokens = lex('0.5E+10'); expectNumberToken(tokens[0], 0, 7, 0.5e10); }); it('should return exception for invalid exponent', () => { expectErrorToken( lex('0.5E-')[0], 4, 5, 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-]', ); expectErrorToken( lex('0.5E-A')[0], 4, 5, 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]', ); }); it('should tokenize number starting with a dot', () => { expectNumberToken(lex('.5')[0], 0, 2, 0.5); }); it('should throw error on invalid unicode', () => { expectErrorToken( lex("'\\u1''bla'")[0], 2, 2, "Lexer Error: Invalid unicode escape [\\u1''b] at column 2 in expression ['\\u1''bla']", ); }); it('should tokenize ?. as operator', () => { expectOperatorToken(lex('?.')[0], 0, 2, '?.'); }); it('should tokenize ?? as operator', () => { expectOperatorToken(lex('??')[0], 0, 2, '??'); }); it('should tokenize number with separator', () => { expectNumberToken(lex('123_456')[0], 0, 7, 123_456); expectNumberToken(lex('1_000_000_000')[0], 0, 13, 1_000_000_000); expectNumberToken(lex('123_456.78')[0], 0, 10, 123_456.78); expectNumberToken(lex('123_456_789.123_456_789')[0], 0, 23, 123_456_789.123_456_789); expectNumberToken(lex('1_2_3_4')[0], 0, 7, 1_2_3_4); expectNumberToken(lex('1_2_3_4.5_6_7_8')[0], 0, 15, 1_2_3_4.5_6_7_8); }); it('should tokenize number starting with an underscore as an identifier', () => { expectIdentifierToken(lex('_123')[0], 0, 4, '_123'); expectIdentifierToken(lex('_123_')[0], 0, 5, '_123_'); expectIdentifierToken(lex('_1_2_3_')[0], 0, 7, '_1_2_3_'); }); it('should throw error for invalid number separators', () => { expectErrorToken( lex('123_')[0], 3, 3, 'Lexer Error: Invalid numeric separator at column 3 in expression [123_]', ); expectErrorToken( lex('12__3')[0], 2, 2, 'Lexer Error: Invalid numeric separator at column 2 in expression [12__3]', ); expectErrorToken( lex('1_2_3_.456')[0], 5, 5, 'Lexer Error: Invalid numeric separator at column 5 in expression [1_2_3_.456]', ); expectErrorToken( lex('1_2_3._456')[0], 6, 6, 'Lexer Error: Invalid numeric separator at column 6 in expression [1_2_3._456]', ); }); it('should tokenize assignment operators', () => { expectOperatorToken(lex('=')[0], 0, 1, '='); expectOperatorToken(lex('+=')[0], 0, 2, '+='); expectOperatorToken(lex('-=')[0], 0, 2, '-='); expectOperatorToken(lex('*=')[0], 0, 2, '*='); expectOperatorToken(lex('a /= b')[1], 2, 4, '/='); expectOperatorToken(lex('%=')[0], 0, 2, '%='); expectOperatorToken(lex('**=')[0], 0, 3, '**='); expectOperatorToken(lex('&&=')[0], 0, 3, '&&='); expectOperatorToken(lex('||=')[0], 0, 3, '||='); expectOperatorToken(lex('??=')[0], 0, 3, '??='); }); it('should tokenize a spread operator', () => { const tokens = lex('{...foo}'); expect(tokens.length).toEqual(4); expectCharacterToken(tokens[0], 0, 1, '{'); expectOperatorToken(tokens[1], 1, 4, '...'); expectIdentifierToken(tokens[2], 4, 7, 'foo'); expectCharacterToken(tokens[3], 7, 8, '}'); }); it('should produce an error for a spread with two dots', () => { const tokens = lex('{..foo}'); expect(tokens.length).toEqual(4); expectCharacterToken(tokens[0], 0, 1, '{'); expectErrorToken( tokens[1], 3, 3, 'Lexer Error: Unexpected character [.] at column 3 in expression [{..foo}]', ); expectIdentifierToken(tokens[2], 3, 6, 'foo'); expectCharacterToken(tokens[3], 6, 7, '}'); }); describe('template literals', () => { it('should tokenize template literal with no interpolations', () => { const tokens: Token[] = lex('`hello world`'); expect(tokens.length).toBe(1); expectStringToken(tokens[0], 0, 13, 'hello world', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize template literal containing strings', () => { expectStringToken(lex('`a "b" c`')[0], 0, 9, `a "b" c`, StringTokenKind.TemplateLiteralEnd); expectStringToken(lex("`a 'b' c`")[0], 0, 9, `a 'b' c`, StringTokenKind.TemplateLiteralEnd); expectStringToken( lex('`a \\`b\\` c`')[0], 0, 11, 'a `b` c', StringTokenKind.TemplateLiteralEnd, ); expectStringToken( lex('`a "\'\\`b\\`\'" c`')[0], 0, 15, `a "'\`b\`'" c`, StringTokenKind.TemplateLiteralEnd, ); }); it('should tokenize unicode inside a template string', () => { const tokens: Token[] = lex('`\\u00A0`'); expect(tokens.length).toBe(1); expect(tokens[0].toString()).toBe('\u00a0'); }); it('should tokenize template literal with an interpolation in the end', () => { const tokens: Token[] = lex('`hello ${name}`'); expect(tokens.length).toBe(5); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectCharacterToken(tokens[3], 13, 14, '}'); expectStringToken(tokens[4], 14, 15, '', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize template literal with an interpolation in the beginning', () => { const tokens: Token[] = lex('`${name} Johnson`'); expect(tokens.length).toBe(5); expectStringToken(tokens[0], 0, 1, '', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 1, 3, '${'); expectIdentifierToken(tokens[2], 3, 7, 'name'); expectCharacterToken(tokens[3], 7, 8, '}'); expectStringToken(tokens[4], 8, 17, ' Johnson', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize template literal with an interpolation in the middle', () => { const tokens: Token[] = lex('`foo${bar}baz`'); expect(tokens.length).toBe(5); expectStringToken(tokens[0], 0, 4, 'foo', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 4, 6, '${'); expectIdentifierToken(tokens[2], 6, 9, 'bar'); expectCharacterToken(tokens[3], 9, 10, '}'); expectStringToken(tokens[4], 10, 14, 'baz', StringTokenKind.TemplateLiteralEnd); }); it('should be able to use interpolation characters inside template string', () => { expectStringToken(lex('`foo $`')[0], 0, 7, 'foo $', StringTokenKind.TemplateLiteralEnd); expectStringToken(lex('`foo }`')[0], 0, 7, 'foo }', StringTokenKind.TemplateLiteralEnd); expectStringToken( lex('`foo $ {}`')[0], 0, 10, 'foo $ {}', StringTokenKind.TemplateLiteralEnd, ); expectStringToken( lex('`foo \\${bar}`')[0], 0, 13, 'foo ${bar}', StringTokenKind.TemplateLiteralEnd, ); }); it('should tokenize template literal with several interpolations', () => { const tokens: Token[] = lex('`${a} - ${b} - ${c}`'); expect(tokens.length).toBe(13); expectStringToken(tokens[0], 0, 1, '', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 1, 3, '${'); expectIdentifierToken(tokens[2], 3, 4, 'a'); expectCharacterToken(tokens[3], 4, 5, '}'); expectStringToken(tokens[4], 5, 8, ' - ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[5], 8, 10, '${'); expectIdentifierToken(tokens[6], 10, 11, 'b'); expectCharacterToken(tokens[7], 11, 12, '}'); expectStringToken(tokens[8], 12, 15, ' - ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[9], 15, 17, '${'); expectIdentifierToken(tokens[10], 17, 18, 'c'); expectCharacterToken(tokens[11], 18, 19, '}'); }); it('should tokenize template literal with an object literal inside the interpolation', () => { const tokens: Token[] = lex('`foo ${{$: true}} baz`'); expect(tokens.length).toBe(9); expectStringToken(tokens[0], 0, 5, 'foo ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 5, 7, '${'); expectCharacterToken(tokens[2], 7, 8, '{'); expectIdentifierToken(tokens[3], 8, 9, '$'); expectCharacterToken(tokens[4], 9, 10, ':'); expectKeywordToken(tokens[5], 11, 15, 'true'); expectCharacterToken(tokens[6], 15, 16, '}'); expectCharacterToken(tokens[7], 16, 17, '}'); expectStringToken(tokens[8], 17, 22, ' baz', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize template literal with template literals inside the interpolation', () => { const tokens: Token[] = lex('`foo ${`hello ${`${a} - b`}`} baz`'); expect(tokens.length).toBe(13); expectStringToken(tokens[0], 0, 5, 'foo ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 5, 7, '${'); expectStringToken(tokens[2], 7, 14, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[3], 14, 16, '${'); expectStringToken(tokens[4], 16, 17, '', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[5], 17, 19, '${'); expectIdentifierToken(tokens[6], 19, 20, 'a'); expectCharacterToken(tokens[7], 20, 21, '}'); expectStringToken(tokens[8], 21, 26, ' - b', StringTokenKind.TemplateLiteralEnd); expectCharacterToken(tokens[9], 26, 27, '}'); expectStringToken(tokens[10], 27, 28, '', StringTokenKind.TemplateLiteralEnd); expectCharacterToken(tokens[11], 28, 29, '}'); expectStringToken(tokens[12], 29, 34, ' baz', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize two template literal right after each other', () => { const tokens: Token[] = lex('`hello ${name}``see ${name} later`'); expect(tokens.length).toBe(10); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectCharacterToken(tokens[3], 13, 14, '}'); expectStringToken(tokens[4], 14, 15, '', StringTokenKind.TemplateLiteralEnd); expectStringToken(tokens[5], 15, 20, 'see ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[6], 20, 22, '${'); expectIdentifierToken(tokens[7], 22, 26, 'name'); expectCharacterToken(tokens[8], 26, 27, '}'); expectStringToken(tokens[9], 27, 34, ' later', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize a concatenated template literal', () => { const tokens: Token[] = lex('`hello ${name}` + 123'); expect(tokens.length).toBe(7); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectCharacterToken(tokens[3], 13, 14, '}'); expectStringToken(tokens[4], 14, 15, '', StringTokenKind.TemplateLiteralEnd); expectOperatorToken(tokens[5], 16, 17, '+'); expectNumberToken(tokens[6], 18, 21, 123); }); it('should tokenize a template literal with a pipe inside an interpolation', () => { const tokens: Token[] = lex('`hello ${name | capitalize}!!!`'); expect(tokens.length).toBe(7); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectOperatorToken(tokens[3], 14, 15, '|'); expectIdentifierToken(tokens[4], 16, 26, 'capitalize'); expectCharacterToken(tokens[5], 26, 27, '}'); expectStringToken(tokens[6], 27, 31, '!!!', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize a template literal with a pipe inside a parenthesized interpolation', () => { const tokens: Token[] = lex('`hello ${(name | capitalize)}!!!`'); expect(tokens.length).toBe(9); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectCharacterToken(tokens[2], 9, 10, '('); expectIdentifierToken(tokens[3], 10, 14, 'name'); expectOperatorToken(tokens[4], 15, 16, '|'); expectIdentifierToken(tokens[5], 17, 27, 'capitalize'); expectCharacterToken(tokens[6], 27, 28, ')'); expectCharacterToken(tokens[7], 28, 29, '}'); expectStringToken(tokens[8], 29, 33, '!!!', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize a template literal in an literal object value', () => { const tokens: Token[] = lex('{foo: `${name}`}'); expect(tokens.length).toBe(9); expectCharacterToken(tokens[0], 0, 1, '{'); expectIdentifierToken(tokens[1], 1, 4, 'foo'); expectCharacterToken(tokens[2], 4, 5, ':'); expectStringToken(tokens[3], 6, 7, '', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[4], 7, 9, '${'); expectIdentifierToken(tokens[5], 9, 13, 'name'); expectCharacterToken(tokens[6], 13, 14, '}'); expectStringToken(tokens[7], 14, 15, '', StringTokenKind.TemplateLiteralEnd); expectCharacterToken(tokens[8], 15, 16, '}'); }); it('should produce an error if a template literal is not terminated', () => { expectErrorToken( lex('`hello')[0], 6, 6, 'Lexer Error: Unterminated template literal at column 6 in expression [`hello]', ); }); it('should produce an error for an unterminated template literal with an interpolation', () => { const tokens: Token[] = lex('`hello ${name}!'); expect(tokens.length).toBe(5); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectCharacterToken(tokens[3], 13, 14, '}'); expectErrorToken( tokens[4], 15, 15, 'Lexer Error: Unterminated template literal at column 15 in expression [`hello ${name}!]', ); }); it('should produce an error for an unterminate template literal interpolation', () => { const tokens: Token[] = lex('`hello ${name!`'); expect(tokens.length).toBe(5); expectStringToken(tokens[0], 0, 7, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[1], 7, 9, '${'); expectIdentifierToken(tokens[2], 9, 13, 'name'); expectOperatorToken(tokens[3], 13, 14, '!'); expectErrorToken( tokens[4], 15, 15, 'Lexer Error: Unterminated template literal at column 15 in expression [`hello ${name!`]', ); }); it('should tokenize tagged template literal with no interpolations', () => { const tokens: Token[] = lex('tag`hello world`'); expect(tokens.length).toBe(2); expectIdentifierToken(tokens[0], 0, 3, 'tag'); expectStringToken(tokens[1], 3, 16, 'hello world', StringTokenKind.TemplateLiteralEnd); }); it('should tokenize nested tagged template literals', () => { const tokens: Token[] = lex('tag`hello ${tag`world`}`'); expect(tokens.length).toBe(7); expectIdentifierToken(tokens[0], 0, 3, 'tag'); expectStringToken(tokens[1], 3, 10, 'hello ', StringTokenKind.TemplateLiteralPart); expectOperatorToken(tokens[2], 10, 12, '${'); expectIdentifierToken(tokens[3], 12, 15, 'tag'); expectStringToken(tokens[4], 15, 22, 'world', StringTokenKind.TemplateLiteralEnd); expectCharacterToken(tokens[5], 22, 23, '}'); expectStringToken(tokens[6], 23, 24, '', StringTokenKind.TemplateLiteralEnd); }); }); describe('regular expressions', () => { it('should tokenize a simple regex', () => { const tokens: Token[] = lex('/abc/'); expect(tokens.length).toBe(1); expectRegExpBodyToken(tokens[0], 0, 5, 'abc'); }); it('should tokenize a regex with flags', () => { const tokens: Token[] = lex('/abc/gim'); expect(tokens.length).toBe(2); expectRegExpBodyToken(tokens[0], 0, 5, 'abc'); expectRegExpFlagsToken(tokens[1], 5, 8, 'gim'); }); it('should tokenize an identifier immediately after a regex', () => { const tokens: Token[] = lex('/abc/ g'); expect(tokens.length).toBe(2); expectRegExpBodyToken(tokens[0], 0, 5, 'abc'); expectIdentifierToken(tokens[1], 6, 7, 'g'); }); it('should tokenize a regex with an escaped slashes', () => { const tokens: Token[] = lex('/^http:\\/\\/foo\\.bar/'); expect(tokens.length).toBe(1); expectRegExpBodyToken(tokens[0], 0, 20, '^http:\\/\\/foo\\.bar'); }); it('should tokenize a regex with un-escaped slashes in a character class', () => { const tokens: Token[] = lex('/[a/]$/'); expect(tokens.length).toBe(1); expectRegExpBodyToken(tokens[0], 0, 7, '[a/]$'); }); it('should tokenize a regex with a backslash', () => { const tokens: Token[] = lex('/a\\w+/'); expect(tokens.length).toBe(1); expectRegExpBodyToken(tokens[0], 0, 6, 'a\\w+'); }); it('should tokenize a regex after an operator', () => { const tokens: Token[] = lex('a = /b/'); expect(tokens.length).toBe(3); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectOperatorToken(tokens[1], 2, 3, '='); expectRegExpBodyToken(tokens[2], 4, 7, 'b'); }); it('should tokenize a regex inside parentheses', () => { const tokens: Token[] = lex('log(/a/)'); expect(tokens.length).toBe(4); expectIdentifierToken(tokens[0], 0, 3, 'log'); expectCharacterToken(tokens[1], 3, 4, '('); expectRegExpBodyToken(tokens[2], 4, 7, 'a'); expectCharacterToken(tokens[3], 7, 8, ')'); }); it('should tokenize a regex at the beggining of an array', () => { const tokens: Token[] = lex('[/a/]'); expect(tokens.length).toBe(3); expectCharacterToken(tokens[0], 0, 1, '['); expectRegExpBodyToken(tokens[1], 1, 4, 'a'); expectCharacterToken(tokens[2], 4, 5, ']'); }); it('should tokenize a regex in the middle of an array', () => { const tokens: Token[] = lex('[1, /a/, 2]'); expect(tokens.length).toBe(7); expectCharacterToken(tokens[0], 0, 1, '['); expectNumberToken(tokens[1], 1, 2, 1); expectCharacterToken(tokens[2], 2, 3, ','); expectRegExpBodyToken(tokens[3], 4, 7, 'a'); expectCharacterToken(tokens[4], 7, 8, ','); expectNumberToken(tokens[5], 9, 10, 2); expectCharacterToken(tokens[6], 10, 11, ']'); }); it('should tokenize a regex inside an object literal', () => { const tokens: Token[] = lex('{a: /b/}'); expect(tokens.length).toBe(5); expectCharacterToken(tokens[0], 0, 1, '{'); expectIdentifierToken(tokens[1], 1, 2, 'a'); expectCharacterToken(tokens[2], 2, 3, ':'); expectRegExpBodyToken(tokens[3], 4, 7, 'b'); expectCharacterToken(tokens[4], 7, 8, '}'); }); it('should tokenize a regex after a negation operator', () => { const tokens: Token[] = lex('log(!/a/.test("1"))'); expect(tokens.length).toBe(10); expectIdentifierToken(tokens[0], 0, 3, 'log'); expectCharacterToken(tokens[1], 3, 4, '('); expectOperatorToken(tokens[2], 4, 5, '!'); expectRegExpBodyToken(tokens[3], 5, 8, 'a'); expectCharacterToken(tokens[4], 8, 9, '.'); expectIdentifierToken(tokens[5], 9, 13, 'test'); expectCharacterToken(tokens[6], 13, 14, '('); expectStringToken(tokens[7], 14, 17, '1', StringTokenKind.Plain); expectCharacterToken(tokens[8], 17, 18, ')'); expectCharacterToken(tokens[9], 18, 19, ')'); }); it('should tokenize a regex after several negation operators', () => { const tokens: Token[] = lex('log(!!!!!!/a/.test("1"))'); expect(tokens.length).toBe(15); expectIdentifierToken(tokens[0], 0, 3, 'log'); expectCharacterToken(tokens[1], 3, 4, '('); expectOperatorToken(tokens[2], 4, 5, '!'); expectOperatorToken(tokens[3], 5, 6, '!'); expectOperatorToken(tokens[4], 6, 7, '!'); expectOperatorToken(tokens[5], 7, 8, '!'); expectOperatorToken(tokens[6], 8, 9, '!'); expectOperatorToken(tokens[7], 9, 10, '!'); expectRegExpBodyToken(tokens[8], 10, 13, 'a'); expectCharacterToken(tokens[9], 13, 14, '.'); expectIdentifierToken(tokens[10], 14, 18, 'test'); expectCharacterToken(tokens[11], 18, 19, '('); expectStringToken(tokens[12], 19, 22, '1', StringTokenKind.Plain); expectCharacterToken(tokens[13], 22, 23, ')'); expectCharacterToken(tokens[14], 23, 24, ')'); }); it('should tokenize a method call on a regex', () => { const tokens: Token[] = lex('/abc/.test("foo")'); expect(tokens.length).toBe(6); expectRegExpBodyToken(tokens[0], 0, 5, 'abc'); expectCharacterToken(tokens[1], 5, 6, '.'); expectIdentifierToken(tokens[2], 6, 10, 'test'); expectCharacterToken(tokens[3], 10, 11, '('); expectStringToken(tokens[4], 11, 16, 'foo', StringTokenKind.Plain); expectCharacterToken(tokens[5], 16, 17, ')'); }); it('should tokenize a method call with a regex parameter', () => { const tokens: Token[] = lex('"foo".match(/abc/)'); expect(tokens.length).toBe(6); expectStringToken(tokens[0], 0, 5, 'foo', StringTokenKind.Plain); expectCharacterToken(tokens[1], 5, 6, '.'); expectIdentifierToken(tokens[2], 6, 11, 'match'); expectCharacterToken(tokens[3], 11, 12, '('); expectRegExpBodyToken(tokens[4], 12, 17, 'abc'); expectCharacterToken(tokens[5], 17, 18, ')'); }); it('should not tokenize a regex preceded by a square bracket', () => { const tokens: Token[] = lex('a[0] /= b'); expect(tokens.length).toBe(6); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectCharacterToken(tokens[1], 1, 2, '['); expectNumberToken(tokens[2], 2, 3, 0); expectCharacterToken(tokens[3], 3, 4, ']'); expectOperatorToken(tokens[4], 5, 7, '/='); expectIdentifierToken(tokens[5], 8, 9, 'b'); }); it('should not tokenize a regex preceded by an identifier', () => { const tokens: Token[] = lex('a / b'); expect(tokens.length).toBe(3); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectOperatorToken(tokens[1], 2, 3, '/'); expectIdentifierToken(tokens[2], 4, 5, 'b'); }); it('should not tokenize a regex preceded by a number', () => { const tokens: Token[] = lex('1 / b'); expect(tokens.length).toBe(3); expectNumberToken(tokens[0], 0, 1, 1); expectOperatorToken(tokens[1], 2, 3, '/'); expectIdentifierToken(tokens[2], 4, 5, 'b'); }); it('should not tokenize a regex that is preceded by a string', () => { const tokens: Token[] = lex('"a" / b'); expect(tokens.length).toBe(3); expectStringToken(tokens[0], 0, 3, 'a', StringTokenKind.Plain); expectOperatorToken(tokens[1], 4, 5, '/'); expectIdentifierToken(tokens[2], 6, 7, 'b'); }); it('should not tokenize a regex preceded by a closing parenthesis', () => { const tokens: Token[] = lex('(a) / b'); expect(tokens.length).toBe(5); expectCharacterToken(tokens[0], 0, 1, '('); expectIdentifierToken(tokens[1], 1, 2, 'a'); expectCharacterToken(tokens[2], 2, 3, ')'); expectOperatorToken(tokens[3], 4, 5, '/'); expectIdentifierToken(tokens[4], 6, 7, 'b'); }); it('should not tokenize a regex that is preceded by a keyword', () => { const tokens: Token[] = lex('this / b'); expect(tokens.length).toBe(3); expectKeywordToken(tokens[0], 0, 4, 'this'); expectOperatorToken(tokens[1], 5, 6, '/'); expectIdentifierToken(tokens[2], 7, 8, 'b'); }); it('should not tokenize a regex preceded by a non-null assertion on an identifier', () => { const tokens: Token[] = lex('foo! / 2'); expect(tokens.length).toBe(4); expectIdentifierToken(tokens[0], 0, 3, 'foo'); expectOperatorToken(tokens[1], 3, 4, '!'); expectOperatorToken(tokens[2], 5, 6, '/'); expectNumberToken(tokens[3], 7, 8, 2); }); it('should not tokenize a regex preceded by a non-null assertion on a function call', () => { const tokens: Token[] = lex('foo()! / 2'); expect(tokens.length).toBe(6); expectIdentifierToken(tokens[0], 0, 3, 'foo'); expectCharacterToken(tokens[1], 3, 4, '('); expectCharacterToken(tokens[2], 4, 5, ')'); expectOperatorToken(tokens[3], 5, 6, '!'); expectOperatorToken(tokens[4], 7, 8, '/'); expectNumberToken(tokens[5], 9, 10, 2); }); it('should not tokenize a regex preceded by a non-null assertion on an array', () => { const tokens: Token[] = lex('[1]! / 2'); expect(tokens.length).toBe(6); expectCharacterToken(tokens[0], 0, 1, '['); expectNumberToken(tokens[1], 1, 2, 1); expectCharacterToken(tokens[2], 2, 3, ']'); expectOperatorToken(tokens[3], 3, 4, '!'); expectOperatorToken(tokens[4], 5, 6, '/'); expectNumberToken(tokens[5], 7, 8, 2); }); it('should not tokenize consecutive regexes', () => { const tokens: Token[] = lex('/ 1 / 2 / 3 / 4'); expect(tokens.length).toBe(6); expectRegExpBodyToken(tokens[0], 0, 5, ' 1 '); expectNumberToken(tokens[1], 6, 7, 2); expectOperatorToken(tokens[2], 8, 9, '/'); expectNumberToken(tokens[3], 10, 11, 3); expectOperatorToken(tokens[4], 12, 13, '/'); expectNumberToken(tokens[5], 14, 15, 4); }); it('should not tokenize regex-like characters inside of a pipe', () => { const tokens: Token[] = lex(`foo / 1000 | date: 'M/d/yy'`); expect(tokens.length).toBe(7); expectIdentifierToken(tokens[0], 0, 3, 'foo'); expectOperatorToken(tokens[1], 4, 5, '/'); expectNumberToken(tokens[2], 6, 10, 1000); expectOperatorToken(tokens[3], 11, 12, '|'); expectIdentifierToken(tokens[4], 13, 17, 'date'); expectCharacterToken(tokens[5], 17, 18, ':'); expectStringToken(tokens[6], 19, 27, 'M/d/yy', StringTokenKind.Plain); }); it('should produce an error for an unterminated regex', () => { expectErrorToken( lex('/a')[0], 2, 2, 'Lexer Error: Unterminated regular expression at column 2 in expression [/a]', ); }); it('should tokenize an arrow function without parenthesis', () => { const tokens = lex('a => a + 1'); expect(tokens.length).toBe(5); expectIdentifierToken(tokens[0], 0, 1, 'a'); expectOperatorToken(tokens[1], 2, 4, '=>'); expectIdentifierToken(tokens[2], 5, 6, 'a'); expectOperatorToken(tokens[3], 7, 8, '+'); expectNumberToken(tokens[4], 9, 10, 1); }); it('should tokenize an arrow function with parenthesis', () => { const tokens = lex('(a, b) => a + b'); expect(tokens.length).toBe(9); expectCharacterToken(tokens[0], 0, 1, '('); expectIdentifierToken(tokens[1], 1, 2, 'a'); expectCharacterToken(tokens[2], 2, 3, ','); expectIdentifierToken(tokens[3], 4, 5, 'b'); expectCharacterToken(tokens[4], 5, 6, ')'); expectOperatorToken(tokens[5], 7, 9, '=>'); expectIdentifierToken(tokens[6], 10, 11, 'a'); expectOperatorToken(tokens[7], 12, 13, '+'); expectIdentifierToken(tokens[8], 14, 15, 'b'); }); }); }); });