import { TokenKind } from './types'; import type { Token, KQLNode, KQLQuery, LiteralNode, ValueListNode, RangeOperator, ParseError } from './types'; import { tokenize, nonWhitespace, decodeUnquotedLiteral, decodeQuotedString } from './lexer'; function truncateValue(value: string): string { return value.length > 20 ? value.slice(0, 17) + '...' : value; } function isLiteralKind(kind: TokenKind): boolean { return kind === TokenKind.Ident || kind === TokenKind.String; } function isRangeOpKind(kind: TokenKind): boolean { return kind === TokenKind.Lt || kind === TokenKind.Lte || kind === TokenKind.Gt || kind === TokenKind.Gte; } function rangeOpOf(kind: TokenKind): RangeOperator | null { switch (kind) { case TokenKind.Lte: return 'lte'; case TokenKind.Gte: return 'gte'; case TokenKind.Lt: return 'lt'; case TokenKind.Gt: return 'gt'; default: return null; } } // ─── Parser class ───────────────────────────────────────────────── class KQLParser { private tokens: Token[]; private pos = 0; errors: ParseError[] = []; constructor(tokens: Token[]) { this.tokens = tokens; } private peek(offset = 0): Token { const idx = this.pos + offset; return idx < this.tokens.length ? this.tokens[idx] : this.tokens[this.tokens.length - 1]; } private next(): Token { if (this.pos >= this.tokens.length) { return this.tokens[this.tokens.length - 1]; } return this.tokens[this.pos++]; } private error(message: string, token: Token): void { this.errors.push({ message, start: token.start, end: token.end }); } // ── Top level ── parse(): KQLQuery { this.pos = 0; this.errors = []; const root = this.parseOrQuery(); const trailing = this.peek(); if (trailing.kind !== TokenKind.EOF) { this.error(`Unexpected token "${truncateValue(trailing.value)}"`, trailing); } return { root, errors: this.errors }; } // ── OrQuery := AndQuery (Or AndQuery)* ── private parseOrQuery(): KQLNode | null { const head = this.parseAndQuery(); if (!head) return null; const children: KQLNode[] = [head]; while (this.peek().kind === TokenKind.Or) { this.next(); const child = this.parseAndQuery(); if (!child) { this.error('Expected an expression after "or"', this.peek()); break; } children.push(child); } if (children.length === 1) return head; return { type: 'or', children, start: head.start, end: children[children.length - 1].end }; } // ── AndQuery := NotQuery (And NotQuery)* ── private parseAndQuery(): KQLNode | null { const head = this.parseNotQuery(); if (!head) return null; const children: KQLNode[] = [head]; while (this.peek().kind === TokenKind.And) { this.next(); const child = this.parseNotQuery(); if (!child) { this.error('Expected an expression after "and"', this.peek()); break; } children.push(child); } if (children.length === 1) return head; return { type: 'and', children, start: head.start, end: children[children.length - 1].end }; } // ── NotQuery := Not SubQuery | SubQuery ── private parseNotQuery(): KQLNode | null { if (this.peek().kind === TokenKind.Not) { const notTok = this.next(); const child = this.parseSubQuery(); if (!child) { this.error('Expected an expression after "not"', this.peek()); return { type: 'not', child: null, start: notTok.start, end: notTok.end }; } return { type: 'not', child, start: notTok.start, end: child.end }; } return this.parseSubQuery(); } // ── SubQuery := '(' OrQuery ')' | NestedQuery ── private parseSubQuery(): KQLNode | null { if (this.peek().kind === TokenKind.LParen) { this.next(); const query = this.parseOrQuery(); const close = this.peek(); if (close.kind === TokenKind.RParen) { this.next(); } else { this.error('Expected closing ")"', close); } return query; } return this.parseNestedOrExpression(); } // ── NestedQuery := Field ':' '{' OrQuery '}' | Expression ── private parseNestedOrExpression(): KQLNode | null { if (isLiteralKind(this.peek().kind) && this.peek(1).kind === TokenKind.Colon && this.peek(2).kind === TokenKind.LCurly) { const field = this.parseLiteral(); if (!field) return null; this.next(); // colon this.next(); // '{' const query = this.parseOrQuery(); const close = this.peek(); if (close.kind === TokenKind.RCurly) { this.next(); } else { this.error('Expected closing "}"', close); } const end = query ? query.end : field.end; return { type: 'nested', field, query, start: field.start, end }; } return this.parseExpression(); } // ── Expression ── private parseExpression(): KQLNode | null { const tok = this.peek(); if (!isLiteralKind(tok.kind)) { if (tok.kind === TokenKind.EOF) return null; this.error(`Unexpected token "${truncateValue(tok.value)}"`, tok); this.next(); return null; } // Field ':' ListOfValues if (this.peek(1).kind === TokenKind.Colon) { const field = this.parseLiteral(); if (!field) return null; const colonTok = this.next(); // field: ( ... ) if (this.peek().kind === TokenKind.LParen) { const list = this.parseValueList(); const end = list ? list.end : colonTok.end; return { type: 'is', field, value: list, start: field.start, end }; } const value = this.parseValue(); if (!value) { this.error('Expected a value after ":"', this.peek()); return { type: 'is', field, value: null, start: field.start, end: colonTok.end }; } return { type: 'is', field, value, start: field.start, end: value.end }; } // Field RangeOp Literal if (isRangeOpKind(this.peek(1).kind)) { const field = this.parseLiteral(); if (!field) return null; const opTok = this.next(); const operator = rangeOpOf(opTok.kind) as RangeOperator; const value = this.parseValue(); if (!value) { this.error(`Expected a value after "${opTok.value}"`, this.peek()); return { type: 'range', field, operator, value: null, start: field.start, end: opTok.end }; } return { type: 'range', field, operator, value, start: field.start, end: value.end }; } // bare Value const value = this.parseLiteral(); if (!value) return null; return { type: 'is', field: null, value, start: value.start, end: value.end }; } // ── Value ── private parseValue(): LiteralNode | null { if (!isLiteralKind(this.peek().kind)) return null; return this.parseLiteral(); } private parseLiteral(): LiteralNode | null { const tok = this.peek(); if (!isLiteralKind(tok.kind)) return null; this.next(); const isQuoted = tok.kind === TokenKind.String; const value = isQuoted ? decodeQuotedString(tok.value) : decodeUnquotedLiteral(tok.value).trim(); return { type: 'literal', value, isQuoted, isWildcard: value.includes('*'), start: tok.start, end: tok.end }; } // ── ListOfValues := '(' OrListOfValues ')' | Value ── private parseValueList(): ValueListNode | null { const open = this.next(); // '(' const values: KQLNode[] = []; const head = this.parseOrListOfValues(); if (head) values.push(head); const close = this.peek(); if (close.kind === TokenKind.RParen) { this.next(); } else { this.error('Expected closing ")"', close); } const end = close.kind === TokenKind.RParen ? close.end : head ? head.end : open.end; return { type: 'valueList', values, start: open.start, end }; } private parseOrListOfValues(): KQLNode | null { const head = this.parseAndListOfValues(); if (!head) return null; const children: KQLNode[] = [head]; while (this.peek().kind === TokenKind.Or) { this.next(); const child = this.parseAndListOfValues(); if (!child) { this.error('Expected a value after "or"', this.peek()); break; } children.push(child); } if (children.length === 1) return head; return { type: 'or', children, start: head.start, end: children[children.length - 1].end }; } private parseAndListOfValues(): KQLNode | null { const head = this.parseNotListOfValues(); if (!head) return null; const children: KQLNode[] = [head]; while (this.peek().kind === TokenKind.And) { this.next(); const child = this.parseNotListOfValues(); if (!child) { this.error('Expected a value after "and"', this.peek()); break; } children.push(child); } if (children.length === 1) return head; return { type: 'and', children, start: head.start, end: children[children.length - 1].end }; } private parseNotListOfValues(): KQLNode | null { if (this.peek().kind === TokenKind.Not) { const notTok = this.next(); const child = this.parseValueListLeaf(); if (!child) { this.error('Expected a value after "not"', this.peek()); return { type: 'not', child: null, start: notTok.start, end: notTok.end }; } return { type: 'not', child, start: notTok.start, end: child.end }; } return this.parseValueListLeaf(); } private parseValueListLeaf(): KQLNode | null { if (this.peek().kind === TokenKind.LParen) { return this.parseValueList(); } return this.parseValue(); } } // ─── Public API ─────────────────────────────────────────────────── export function parse(input: string): KQLQuery { const tokens = nonWhitespace(tokenize(input)); const parser = new KQLParser(tokens); return parser.parse(); }