import { tokenize, nonWhitespace, decodeUnquotedLiteral, decodeQuotedString } from '../parser/lexer'; import { TokenKind } from '../parser/types'; function kinds(input: string): TokenKind[] { return nonWhitespace(tokenize(input)) .filter((t) => t.kind !== TokenKind.EOF) .map((t) => t.kind); } function values(input: string): string[] { return nonWhitespace(tokenize(input)) .filter((t) => t.kind !== TokenKind.EOF) .map((t) => t.value); } describe('KQL lexer', () => { // ── Basic tokens ── it('tokenizes parentheses and braces', () => { expect(kinds('(){ }')).toEqual([TokenKind.LParen, TokenKind.RParen, TokenKind.LCurly, TokenKind.RCurly]); }); it('tokenizes colon', () => { expect(kinds(':')).toEqual([TokenKind.Colon]); }); it('tokenizes range operators (<= before <)', () => { expect(kinds('< <= > >=')).toEqual([TokenKind.Lt, TokenKind.Lte, TokenKind.Gt, TokenKind.Gte]); }); // ── Strings ── it('tokenizes closed quoted string', () => { const t = nonWhitespace(tokenize('"hello"')); expect(t[0].kind).toBe(TokenKind.String); expect(t[0].value).toBe('"hello"'); }); it('tokenizes escaped quote inside string', () => { const t = nonWhitespace(tokenize('"a\\"b"')); expect(t[0].kind).toBe(TokenKind.String); expect(t[0].value).toBe('"a\\"b"'); }); it('tokenizes unclosed string to end of input', () => { const t = nonWhitespace(tokenize('"unclosed')); expect(t[0].kind).toBe(TokenKind.String); expect(t[0].value).toBe('"unclosed'); }); // ── Unquoted literals ── it('tokenizes identifiers including dots', () => { expect(values('machine.os')).toEqual(['machine.os']); }); it('keeps wildcards inside unquoted literals', () => { expect(values('win*')).toEqual(['win*']); expect(values('*')).toEqual(['*']); }); it('absorbs plain whitespace into unquoted literals', () => { // "Windows 10" as a bare value is a single literal (grammar-faithful) expect(values('Windows 10')).toEqual(['Windows 10']); }); it('splits literals on special characters', () => { expect(kinds('response:200')).toEqual([TokenKind.Ident, TokenKind.Colon, TokenKind.Ident]); expect(kinds('field:(a)')).toEqual([TokenKind.Ident, TokenKind.Colon, TokenKind.LParen, TokenKind.Ident, TokenKind.RParen]); }); // ── Keyword boundaries ── it('recognizes and/or/not surrounded by whitespace', () => { expect(kinds('a and b')).toEqual([TokenKind.Ident, TokenKind.And, TokenKind.Ident]); expect(kinds('a or b')).toEqual([TokenKind.Ident, TokenKind.Or, TokenKind.Ident]); expect(kinds('not foo')).toEqual([TokenKind.Not, TokenKind.Ident]); }); it('is case insensitive for keywords', () => { expect(kinds('a AND b OR NOT c')).toEqual([TokenKind.Ident, TokenKind.And, TokenKind.Ident, TokenKind.Or, TokenKind.Not, TokenKind.Ident]); }); it('does not split identifiers containing keyword substrings', () => { expect(values('android')).toEqual(['android']); expect(values('annotated')).toEqual(['annotated']); }); it('does not treat and/or without trailing whitespace as keywords', () => { // Grammar: And/Or = Space+ keyword Space+ — at EOF they stay part of the literal expect(values('a and')).toEqual(['a and']); expect(values('a or')).toEqual(['a or']); }); it('treats a leading and/or as a literal (no preceding whitespace)', () => { expect(values('and foo')).toEqual(['and foo']); }); it('recognizes not after a colon (no preceding whitespace required)', () => { expect(kinds('foo:not bar')).toEqual([TokenKind.Ident, TokenKind.Colon, TokenKind.Not, TokenKind.Ident]); }); it('splits literals before "not" followed by whitespace (grammar quirk)', () => { // matches the peggy grammar: !Keyword lookahead with 'not'i Space expect(kinds('cannot ')).toEqual([TokenKind.Ident, TokenKind.Not]); expect(values('cannot ')).toEqual(['can', 'not']); }); // ── Escapes ── it('decodes escaped special characters', () => { expect(decodeUnquotedLiteral('a\\:b')).toBe('a:b'); expect(decodeUnquotedLiteral('\\(x\\)')).toBe('(x)'); expect(decodeUnquotedLiteral('\\\\')).toBe('\\'); }); it('decodes escaped whitespace and unicode sequences', () => { expect(decodeUnquotedLiteral('a\\tb')).toBe('a\tb'); expect(decodeUnquotedLiteral('a\\nb')).toBe('a\nb'); expect(decodeUnquotedLiteral('a\\rb')).toBe('a\rb'); expect(decodeUnquotedLiteral('\\u0041')).toBe('A'); }); it('decodes escaped keywords', () => { expect(decodeUnquotedLiteral('\\and')).toBe('and'); expect(decodeUnquotedLiteral('\\or')).toBe('or'); // EscapedWhitespace (\n) wins over EscapedKeyword (\not) — grammar-faithful order expect(decodeUnquotedLiteral('\\not')).toBe('\not'); }); it('keeps invalid escapes verbatim (tolerant)', () => { expect(decodeUnquotedLiteral('\\x')).toBe('\\x'); }); it('decodes quoted string escapes', () => { expect(decodeQuotedString('"a\\"b"')).toBe('a"b'); expect(decodeQuotedString('"a\\\\b"')).toBe('a\\b'); expect(decodeQuotedString('"tab\\there"')).toBe('tab\there'); expect(decodeQuotedString('"\\u0041"')).toBe('A'); expect(decodeQuotedString('"back\\slash"')).toBe('back\\slash'); }); it('decodes escaped keyword inside unquoted literal', () => { expect(decodeUnquotedLiteral('foo \\and bar')).toBe('foo and bar'); }); // ── Token spans ── it('records token start/end offsets', () => { const t = nonWhitespace(tokenize('response:200')); expect(t[0]).toMatchObject({ kind: TokenKind.Ident, start: 0, end: 8 }); expect(t[1]).toMatchObject({ kind: TokenKind.Colon, start: 8, end: 9 }); expect(t[2]).toMatchObject({ kind: TokenKind.Ident, start: 9, end: 12 }); }); it('emits EOF sentinel', () => { const t = tokenize(''); expect(t).toHaveLength(1); expect(t[0].kind).toBe(TokenKind.EOF); }); // ── A full query ── it('tokenizes a full query', () => { const t = nonWhitespace(tokenize('response >= 400 and machine.os: "win 10"')).filter((x) => x.kind !== TokenKind.EOF); expect(t.map((x) => x.kind)).toEqual([TokenKind.Ident, TokenKind.Gte, TokenKind.Ident, TokenKind.And, TokenKind.Ident, TokenKind.Colon, TokenKind.String]); }); });