@mfissehaye/string-to-drizzle-orm-filters
Version:
184 lines (164 loc) • 6.8 kB
text/typescript
import { CallExpression, NumberLiteral, Program, StringLiteral } from "./ast";
import { Lexer, Token, TokenType } from "./lexer";
export class ParserError extends Error {
constructor(message: string, public token?: Token) {
super(message);
this.name = 'ParserError';
}
}
/**
* The Parser class takes a Lexer instance and constructs an Abstraact Syntax Tree (AST)
* from the stream of tokens
*/
export class Parser {
private lexer: Lexer
private lookahead: Token | null = null; // The next token to be consumed
constructor(lexer: Lexer) {
this.lexer = lexer;
}
public parse(): Program {
this.lookahead = this.lexer.nextToken(); // Initialize lookahead
const expression = this.parseExpression();
if (this.lookahead.type !== TokenType.EOF) {
throw new ParserError(
`Unexpected token '${this.lookahead.value}' at position ${this.lookahead.position}. Expected end of input.`,
this.lookahead,
)
}
return { kind: 'Program', expression }
}
private consume(expectedType: TokenType, errorMessage?: string): Token {
const token = this.lookahead;
if (!token || token.type !== expectedType) {
throw new ParserError(
errorMessage ||
`Unexpected token '${token?.value}' (type ${token?.type}). Expected ${expectedType}.`,
token!,
)
}
this.lookahead = this.lexer.nextToken(); // Move to the next token
return token;
}
/**
* Checks if the current lookahead matches the given type without consuming it.
*/
private match(type: TokenType): boolean {
return this.lookahead?.type === type
}
/**
* @Parses a general expression.
* Currently, this only delegates to logical expressions.
*/
private parseExpression(): CallExpression {
return this.parseLogicalExpression();
}
private parseLogicalExpression(): CallExpression {
let expression = this.parsePrimaryExpression();
while (
this.match(TokenType.Identifier) &&
(this.lookahead?.value === 'and' || this.lookahead?.value === 'or')
) {
const operatorToken = this.consume(TokenType.Identifier)
// const rightExpression = this.parsePrimaryExpression(); // this will be the second argument after a comma
throw new ParserError(`Unexpected logical operator '${operatorToken.value}' not in a function call.`, operatorToken)
}
return expression
}
/**
* Parses a primary expression, which can be a CallExpression or a parenthesized expression.
*/
private parsePrimaryExpression(): CallExpression {
if (this.match(TokenType.LParen)) {
this.consume(TokenType.LParen);
const expression = this.parseLogicalExpression(); // Recursively parse the expression inside the parentheses
this.consume(
TokenType.RParen,
`Expected ')' to close expression started at position ${expression.kind === 'CallExpression' ? this.lookahead?.position : 'unknown'}.`
)
return expression;
} else if (this.match(TokenType.Identifier)) {
return this.parseCallExpression();
} else {
throw new ParserError(
`Unexpected token '${this.lookahead?.value}' (type ${this.lookahead?.type}). Expected a function call or a '('.`,
this.lookahead!,
)
}
}
/**
* Parses a function call expression (e.g., `eq("col", "val")`).
*/
private parseCallExpression(): CallExpression {
const functionNameToken = this.consume(
TokenType.Identifier,
`Expected a function name (identifier) but got '${this.lookahead?.value}' (type ${this.lookahead?.type}).`,
);
this.consume(
TokenType.LParen,
`Expected '(' after function name '${functionNameToken.value}'.`
)
const args: (StringLiteral | NumberLiteral | CallExpression)[] = this.parseArguments();
this.consume(
TokenType.RParen,
`Expected ')' to close function call '${functionNameToken.value}'.`,
)
return {
kind: 'CallExpression',
functionName: functionNameToken.value,
args,
}
}
/**
* Parses the arguments within a function call.
* Arguments can be string literals or nested call expressions.
*/
private parseArguments(): (StringLiteral | NumberLiteral | CallExpression)[] {
const args: (StringLiteral | NumberLiteral | CallExpression)[] = [];
// Check for empty arguments (.e.g., `func()`)
if (this.match(TokenType.RParen)) {
return args;
}
// Parse the first argument
args.push(this.parseArgument());
// Parse subsequent arguments separated by commas
while (this.match(TokenType.Comma)) {
this.consume(TokenType.Comma);
args.push(this.parseArgument());
}
return args;
}
private parseArgument(): StringLiteral | NumberLiteral | CallExpression {
if (this.match(TokenType.StringLiteral)) {
const stringToken = this.consume(
TokenType.StringLiteral,
`Expected a staring literal but got '${this.lookahead?.value}' (type ${this.lookahead?.type}).`,
);
return {
kind: 'StringLiteral',
value: stringToken.value,
}
} else if (this.match(TokenType.NumberLiteral)) {
const numberToken = this.consume(
TokenType.NumberLiteral,
`Expected a number literal but got '${this.lookahead?.value}' (type ${this.lookahead?.type}).`,
);
// Convert the string value to a number
const numericValue = parseFloat(numberToken.value);
if (isNaN(numericValue)) {
throw new ParserError(`Invalid number literal: '${numberToken.value}'`, numberToken);
}
return {
kind: 'NumberLiteral',
value: numericValue,
};
} else if (this.match(TokenType.Identifier)) {
// Allow nested function calls as arguments (e.g., `and(eq(...), or(...))`)
return this.parseCallExpression();
} else {
throw new ParserError(
`Unexpected token '${this.lookahead?.value}' (type ${this.lookahead?.type}). Expected a string literal or a nested function call as an argument.`,
this.lookahead!,
)
}
}
}