import type { Expressions, TypedExpression, IEnumerable, StrictExpressions, StrictTypedExpression, UnknownExpression } from '../expression.js'; import { ExpressionType } from '../expression-type.js'; import { BinaryExpression } from '../binary-expression.js'; import { UnaryExpression } from '../unary-expression.js'; import { ParameterExpression } from '../parameter-expression.js'; import { ConstantExpression } from '../constant-expression.js'; import { TypedLambdaExpression, type Parameters } from '../lambda-expression.js'; import { MemberExpression } from '../member-expression.js'; import { CallExpression } from '../call-expression.js'; import { ApplySymbolExpression } from '../apply-symbol-expression.js'; import { NewExpression } from '../new-expression.js'; import type { IVisitable } from '../visitable.js'; import { FormatExpression } from '../../parser.js'; import { TernaryExpression } from '../ternary-expression.js'; import { AssignmentExpression } from '../assignment-expression.js'; /** * Defines a comparison function between two values of type T. * @template T - The type of values being compared */ export type EqualityComparer = (a: T, b: T) => boolean; /** * Base class for implementing the Visitor pattern for expression tree processing. */ export class ExpressionVisitor { /** * Processes an assignment expression by visiting its left and right operands. * @template T - The expression's value type * @param {AssignmentExpression} expression - The assignment expression to process. * @returns {AssignmentExpression} The processed assignment expression. */ visitAssign( expression: AssignmentExpression ): AssignmentExpression { const left = this.visit(expression.left); const right = this.visit(expression.right); return left !== expression.left || right !== expression.right ? new AssignmentExpression(left, expression.operator, right) : expression; } /** * Visits an expression and returns the processed result. * @template T - The type of the expression's output value * @param {StrictTypedExpression} expression - A strongly-typed expression * @returns {StrictTypedExpression} The processed expression */ public visit(expression: StrictTypedExpression): StrictTypedExpression; /** * Visits a typed expression and returns the processed result. * @template T - The type of the expression's output * @param {TypedExpression} expression - A typed expression * @returns {TypedExpression} The processed expression */ public visit(expression: TypedExpression): TypedExpression; /** * Visits a strict expression and returns the processed result. * @param {StrictExpressions} expression - A strict expression node * @returns {StrictExpressions} The processed expression */ public visit(expression: StrictExpressions): StrictExpressions; /** * Visits an expression implementing the IVisitable interface. * @template T - The visitor type * @template U - The return type * @param {IVisitable} expression - A visitable expression * @returns {Expressions} The processed expression result */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public visit(expression: IVisitable): Expressions // eslint-disable-next-line @typescript-eslint/no-explicit-any public visit(expression: IVisitable): Expressions { return expression.accept(this); } /** * Handles unknown expression types by attempting to visit them. * @param {UnknownExpression} expression - An unknown expression type * @returns {Expressions} The processed expression or throws an error */ visitUnknown(expression: UnknownExpression) { if (expression.accept) return expression.accept(this); throw new Error("unsupported type"); } /** * Processes a format expression by visiting its components. * @template TOutput - The formatted output type * @param {FormatExpression} expression - The format expression to process * @returns {FormatExpression} The processed format expression */ visitFormat(expression: FormatExpression) { const source = this.visit(expression.lhs); if (expression.settings) { const settings = this.visit(expression.settings); if (source !== expression.lhs || settings !== expression.settings) { return new FormatExpression(source, expression.formatter, settings); } } if (source !== expression.lhs) return new FormatExpression(source, expression.formatter, null); return expression; } /** * Processes a new expression by visiting its initialization members. * @template T - The expression's value type * @param {NewExpression} expression - The new expression to process * @returns {StrictExpressions} The processed new expression */ visitNew(expression: NewExpression): StrictExpressions { const members = this.visitArray(expression.init as any) as MemberExpression[]; if (members !== expression.init) return new NewExpression(...members); return expression; } /** * Processes an apply symbol expression by visiting its components. * @template T - The source expression type * @template U - The result type after applying the symbol * @param {ApplySymbolExpression} expression - The apply symbol expression to process * @returns {StrictExpressions} The processed expression */ visitApplySymbol(expression: ApplySymbolExpression): StrictExpressions { const source = this.visit(expression.source); const arg = this.visit(expression.argument); if (source !== expression.source || arg !== expression.argument) { if (!this.isTypedExpression(source)) throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`); return new ApplySymbolExpression(source, expression.symbol, arg); } return expression; } /** * Processes a call expression by visiting its components. * @template T - The source expression type * @template TMethod - The method key type * @param {CallExpression} expression - The call expression to process * @returns {StrictExpressions} The processed expression */ visitCall(expression: CallExpression): StrictExpressions { const source = this.visit(expression.source); const args = this.visitArray(expression.arguments as StrictExpressions[]) as StrictExpressions[]; if (source !== expression.source || args !== expression.arguments) { if (!this.isTypedExpression(source)) throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`); return new CallExpression(source, expression.method, args, expression.optional); } return expression; } /** * Processes a member expression by visiting its components. * @template T - The source expression type * @template TMember - The member key type * @param {MemberExpression} expression - The member expression to process * @returns {StrictExpressions} The processed expression */ visitMember( expression: MemberExpression ): StrictExpressions { const source = expression.source ? this.visit(expression.source) : expression.source; const member = this.visit(expression.member); if (source !== expression.source || member !== expression.member) { if (!this.isTypedExpression(source)) throw new Error(`Source expression of type ${source?.['type']} cannot be treated as typed`); return new MemberExpression( source as TypedExpression, member, expression.optional ); } return expression; } /** * Checks if an expression is a typed expression. * @param {Expressions} expression - The expression to check * @returns {expression is TypedExpression} Type predicate indicating if the expression is typed */ isTypedExpression(expression: Expressions): expression is TypedExpression { return expression != null && ( expression.type === ExpressionType.ConstantExpression || expression.type === ExpressionType.ParameterExpression || expression.type === ExpressionType.MemberExpression || expression.type === ExpressionType.ApplySymbolExpression || expression.type === ExpressionType.NewExpression || expression.type === ExpressionType.Format || expression.type === ExpressionType.UnaryExpression || (expression.type === ExpressionType.TernaryExpression && this.isTypedExpression(expression.second) && this.isTypedExpression(expression.third)) ); } /** * Processes a lambda expression by visiting its body and parameters. * @template T - The lambda's function type * @param {TypedLambdaExpression} expression - The lambda expression to process * @returns {StrictExpressions} The processed expression */ visitLambda unknown>( expression: TypedLambdaExpression ): StrictExpressions { const parameters = this.visitArray(expression.parameters as Expressions[]) as unknown as Parameters; const body = this.visit(expression.body); if (body !== expression.body || parameters !== expression.parameters) return new TypedLambdaExpression(body, ...parameters); return expression; } /** * Default equality comparer for items. * @template T - The item type * @param {T} a - First item * @param {T} b - Second item * @returns {boolean} True if items are strictly equal */ private static defaultComparer(a: T, b: T): boolean { return a === b; } /** * Visits items in an enumerable collection and applies transformation logic. * @template T - The item type * @template U - The transformed item type * @param {IEnumerable} source - Source collection * @param {addToNew} addToNew - Callback to add transformed items * @param {visitSingle} visitSingle - Transformation function per item * @param {EqualityComparer} compare - Optional comparison function */ visitEnumerable( source: IEnumerable, addToNew: (item: U, index: number) => void, visitSingle: (item: T, index: number) => U, compare?: EqualityComparer ): void { const comparator = compare ?? ExpressionVisitor.defaultComparer; let temp: T[] = []; let index = 0; for (const item of source) { const transformed = visitSingle.call(this, item, index); if (!comparator(transformed, item)) { if (temp) { temp.forEach(addToNew); temp = null; } } if (temp) temp.push(item); else addToNew(transformed, index); index++; } } /** * Visits an array of expressions and applies transformation logic. * @template T - The input expression type * @template U - The output expression type * @param {T[]} expressions - Array of expressions to visit * @param {preVisit} preVisit - Optional callback before visiting * @returns {U[]} The transformed array of expressions */ visitArray, U extends T>( expressions: T[], preVisit?: (expression: T, index: number) => void ): U[] { let result: U[] | null = null; this.visitEnumerable( expressions, (item, i) => { if (!result) { result = new Array(expressions.length); } result[i] = item; }, (e, i) => { preVisit?.(e, i); return this.visit(e) as unknown as U; } ); return result || (expressions as U[]); } /** * Processes a constant expression (no changes required). * @param {ConstantExpression} expression - The constant expression * @returns {StrictExpressions} The same expression */ visitConstant(expression: ConstantExpression): StrictExpressions { return expression; } /** * Processes a parameter expression (no changes required). * @param {ParameterExpression} expression - The parameter expression * @returns {StrictExpressions} The same expression */ visitParameter(expression: ParameterExpression): StrictExpressions { return expression; } /** * Processes a unary expression by visiting its operand. * @param {UnaryExpression} expression - The unary expression to process * @returns {Expressions} The processed unary expression */ visitUnary(expression: UnaryExpression): Expressions { const operand = this.visit(expression.operand); return operand !== expression.operand ? new UnaryExpression(operand, expression.operator) : expression; } /** * Processes a binary expression by visiting its left/right operands. * @template T - The expression's value type * @param {BinaryExpression} expression - The binary expression to process * @returns {BinaryExpression} The processed binary expression */ visitBinary( expression: BinaryExpression ): BinaryExpression { const left = this.visit(expression.left); const right = this.visit(expression.right); return left !== expression.left || right !== expression.right ? new BinaryExpression(left, expression.operator, right) : expression; } /** * Processes a ternary expression by visiting its branches. * @template T - The expression's value type * @param {TernaryExpression} expression - The ternary expression to process * @returns {TernaryExpression} The processed ternary expression */ visitTernary( expression: TernaryExpression ): TernaryExpression { const first = this.visit(expression.first); const second = this.visit(expression.second); const third = this.visit(expression.third); return ( first !== expression.first || second !== expression.second || third !== expression.third ) ? new TernaryExpression(first, expression.operator, second, third) : expression; } }