UNPKG

@zvenigora/ng-eval-core

Version:

An expression evaluator for Angular

1,602 lines (1,580 loc) 127 kB
import * as i0 from '@angular/core'; import { Injectable } from '@angular/core'; import { sha256 } from 'js-sha256'; import { defaultOptions, parse as parse$1, version } from 'acorn'; import * as walk from 'acorn-walk'; /** * Represents a registry that stores key-value pairs. */ class BaseRegistry { /** * Creates a new Registry instance. * @param entries - Optional array of key-value pairs to initialize the registry. */ constructor(entries) { this._registry = new Map(); this.type = 'BaseRegistry'; /** * Options for the base registry. */ this.options = { caseInsensitive: false }; if (entries && Array.isArray(entries)) { for (const [key, value] of entries) { this.set(key, value); } } else if (entries && typeof entries === 'object') { for (const [key, value] of Object.entries(entries)) { this.set(key, value); } } } /** * Creates a new Registry instance from an object. * @param object - The object containing key-value pairs. * @returns A new Registry instance. */ static fromObject(object) { const registry = new BaseRegistry(); for (const [key, value] of Object.entries(object)) { registry.set(key, value); } return registry; } /** * Retrieves the value associated with the specified key. * @param key - The key to retrieve the value for. * @returns The value associated with the key, or undefined if the key does not exist. */ get(key) { return this._registry.get(key); } /** * Sets the value for the specified key. * @param key - The key to set the value for. * @param value - The value to set. */ set(key, value) { this._registry.set(key, value); } /** * Checks if the registry contains the specified key. * @param key - The key to check. * @returns True if the registry contains the key, false otherwise. */ has(key) { return this._registry.has(key); } /** * Deletes the key-value pair associated with the specified key. * @param key - The key to delete. * @returns True if the key-value pair was deleted, false if the key does not exist. */ delete(key) { return this._registry.delete(key); } /** * Clears all key-value pairs from the registry. */ clear() { this._registry.clear(); } /** * Gets the number of key-value pairs in the registry. */ get size() { return this._registry.size; } /** * Gets an iterator for the keys in the registry. */ get keys() { return this._registry.keys(); } /** * Gets an iterator for the values in the registry. */ get values() { return this._registry.values(); } /** * Gets an iterator for the key-value pairs in the registry. */ get entries() { return this._registry.entries(); } /** * Executes a provided function once for each key-value pair in the registry. * @param callbackfn - The function to execute for each key-value pair. * @param thisArg - Optional value to use as `this` when executing the callback function. */ forEach(callbackfn, thisArg) { this._registry.forEach(callbackfn, thisArg); } /** * Returns an iterator for the entries of the registry. * @returns An iterator that yields key-value pairs of type [TKey, TValue]. */ [Symbol.iterator]() { return this._registry.entries(); } /** * Converts a BaseRegistry instance to an object. * @param registry The BaseRegistry instance to convert. * @returns An object representing the key-value pairs in the registry. */ toObject() { const object = Object.fromEntries(this.entries); return object; } } // some code here: // https://stackoverflow.com/questions/50019920/javascript-map-key-value-pairs-case-insensitive-search /** * Represents a case-insensitive registry that maps keys to values. */ class CaseInsensitiveRegistry { /** * Creates a new instance of CaseInsensitiveRegistry. * @param entries - Optional initial entries to populate the registry. */ constructor(entries) { this.registry = new Map(); this.keysMap = new Map(); this.type = 'CaseInsensitiveRegistry'; /** * Options for the registry. */ this.options = { caseInsensitive: true }; if (entries && Array.isArray(entries)) { for (const [key, value] of entries) { this.set(key, value); } } else if (entries && typeof entries === 'object') { for (const [key, value] of Object.entries(entries)) { this.set(key, value); } } } /** * Creates a new CaseInsensitiveRegistry from an object. * @param object - The object containing key-value pairs. * @returns A new CaseInsensitiveRegistry instance. */ static fromObject(object) { const registry = new CaseInsensitiveRegistry(); for (const [key, value] of Object.entries(object)) { registry.set(key, value); } return registry; } /** * Retrieves the value associated with the specified key. * @param key - The key. * @returns The value associated with the key, or undefined if the key is not found. */ get(key) { return this.registry.get(this.getKey(key)); } /** * Sets a key-value pair in the registry. * @param key - The key. * @param value - The value. */ set(key, value) { const newKey = this.getKey(key); this.registry.set(newKey, value); this.keysMap.set(newKey, key); } /** * Checks if the registry contains the specified key. * @param key - The key. * @returns True if the key is found, false otherwise. */ has(key) { return this.registry.has(this.getKey(key)); } /** * Deletes the key-value pair associated with the specified key. * @param key - The key. * @returns True if the key-value pair is deleted, false if the key is not found. */ delete(key) { const newKey = this.getKey(key); this.keysMap.delete(newKey); return this.registry.delete(newKey); } /** * Clears all key-value pairs from the registry. */ clear() { this.registry.clear(); this.keysMap.clear(); } /** * Gets the number of key-value pairs in the registry. */ get size() { return this.registry.size; } /** * Gets an iterator for the keys in the registry. */ get keys() { return this.keysMap.values(); } /** * Gets an iterator for the values in the registry. */ get values() { return this.registry.values(); } /** * Gets an iterator for the key-value pairs in the registry. */ get entries() { const keys = this.keysMap.values(); const values = this.registry.values(); const entries = new Array; for (let i = 0; i < this.registry.size; i++) { const keyResult = keys.next(); const valueResult = values.next(); if (!keyResult.done && !valueResult.done) { entries.push([keyResult.value, valueResult.value]); } } return entries.values(); } /** * Executes a provided function once for each key-value pair in the registry. * @param callbackfn - The function to execute for each key-value pair. */ forEach(callbackfn) { this.registry.forEach(callbackfn); } /** * Returns an iterator for the entries of the registry. * @returns An iterator that yields key-value pairs of type [string, unknown]. */ [Symbol.iterator]() { return this.registry[Symbol.iterator](); } getKey(key) { const keyLowerCase = typeof key === 'string' ? key.toLowerCase() : key; return keyLowerCase; } /** * Converts a BaseRegistry instance to an object. * @param registry The BaseRegistry instance to convert. * @returns An object representing the key-value pairs in the registry. */ toObject() { const object = Object.fromEntries(this.entries); return object; } } class Registry { /** * Creates a new Registry instance. * @param entries - Optional array of key-value pairs to initialize the registry. */ constructor(entries, options = { caseInsensitive: false }) { this.type = 'Registry'; /** * Options for the registry. */ this.options = { caseInsensitive: false }; this.options = options; const caseInsensitive = typeof options?.['caseInsensitive'] === 'boolean' ? options['caseInsensitive'] : false; this._registry = caseInsensitive ? new CaseInsensitiveRegistry(entries) : new BaseRegistry(entries); } /** * Creates a new Registry instance from an object. * @param object - The object containing key-value pairs. * @returns A new Registry instance. */ static fromObject(object, options = { caseInsensitive: false }) { const entries = Object.entries(object); const registry = new Registry(entries, options); return registry; } /** * Retrieves the value associated with the specified key. * @param key - The key to retrieve the value for. * @returns The value associated with the key, or undefined if the key does not exist. */ get(key) { return this._registry.get(key); } /** * Sets the value for the specified key. * @param key - The key to set the value for. * @param value - The value to set. */ set(key, value) { this._registry.set(key, value); } /** * Checks if the registry contains the specified key. * @param key - The key to check. * @returns True if the registry contains the key, false otherwise. */ has(key) { return this._registry.has(key); } /** * Deletes the key-value pair associated with the specified key. * @param key - The key to delete. * @returns True if the key-value pair was deleted, false if the key does not exist. */ delete(key) { return this._registry.delete(key); } /** * Clears all key-value pairs from the registry. */ clear() { this._registry.clear(); } /** * Gets the number of key-value pairs in the registry. */ get size() { return this._registry.size; } /** * Gets an iterator for the keys in the registry. */ get keys() { return this._registry.keys; } /** * Gets an iterator for the values in the registry. */ get values() { return this._registry.values; } /** * Gets an iterator for the key-value pairs in the registry. */ get entries() { return this._registry.entries; } /** * Executes a provided function once for each key-value pair in the registry. * @param callbackfn - The function to execute for each key-value pair. * @param thisArg - Optional value to use as `this` when executing the callback function. */ forEach(callbackfn, thisArg) { this._registry.forEach(callbackfn, thisArg); } /** * Returns an iterator for the entries of the registry. * @returns An iterator that yields key-value pairs of type [TKey, TValue]. */ [Symbol.iterator]() { return this._registry.entries; } /** * Converts a BaseRegistry instance to an object. * @param registry The BaseRegistry instance to convert. * @returns An object representing the key-value pairs in the registry. */ toObject() { const object = Object.fromEntries(this.entries); return object; } } /** * Represents a generic queue data structure. * @template T The type of elements stored in the queue. */ class Queue { /** * Creates a new instance of the Queue class. * @param _queue Optional initial array of elements. */ constructor(_queue = []) { this._queue = _queue; } /** * Gets the number of elements in the queue. */ get length() { return this._queue.length; } /** * Removes and returns the first element in the queue. * @returns The first element in the queue, or undefined if the queue is empty. */ dequeue() { return this._queue.shift(); } /** * Adds an element to the end of the queue. * @param value The element to add to the queue. * @returns The new length of the queue. */ enqueue(value) { return this._queue.push(value); } /** * Returns the first element in the queue without removing it. * @returns The first element in the queue. */ peek() { return this._queue[0]; } } /** * Represents a stack data structure with proper memory management. * @template T The type of elements in the stack. */ class Stack { /** * Creates a new instance of the Stack class. * @param _stack An optional array to initialize the stack. */ constructor(_stack = []) { this._stack = _stack; this._disposed = false; } /** * Check if the stack has been disposed */ checkDisposed() { if (this._disposed) { throw new Error('Stack has been disposed and cannot be used'); } } /** * Gets the number of elements in the stack. */ get length() { this.checkDisposed(); return this._stack.length; } /** * Returns the top element of the stack without removing it. * @returns The top element of the stack. */ peek() { this.checkDisposed(); return this._stack[this._stack.length - 1]; } /** * Removes and returns the top element of the stack. * @returns The top element of the stack, or undefined if the stack is empty. */ pop() { this.checkDisposed(); return this._stack.pop(); } /** * Adds an element to the top of the stack. * @param value The element to be added to the stack. * @returns The new length of the stack. */ push(value) { this.checkDisposed(); return this._stack.push(value); } /** * Swaps the positions of the top two elements in the stack. */ swap() { this.checkDisposed(); const length = this._stack.length; const temp = this._stack[length - 1]; this._stack[length - 1] = this._stack[length - 2]; this._stack[length - 2] = temp; } /** * Returns the elements of the stack as an array in reverse order. * * @returns An array containing the elements of the stack in reverse order. */ asArray() { this.checkDisposed(); return [...this._stack].reverse(); } /** * Clears all elements from the stack */ clear() { this.checkDisposed(); // Clear all references to help with garbage collection if (this._stack) { this._stack.length = 0; } } /** * Properly dispose of the stack and clean up memory */ dispose() { if (!this._disposed) { this._disposed = true; // Clear the stack array and nullify reference if (this._stack) { this._stack.length = 0; this._stack = undefined; } } } /** * Check if the stack has been disposed */ isDisposed() { return this._disposed; } } /** * Represents a cache implementation that stores key-value pairs. * @template TValue The type of values stored in the cache. */ class Cache { /** * Creates a new instance of the Cache class. * @param _maxCacheSize The maximum size of the cache. */ constructor(_maxCacheSize) { this._maxCacheSize = _maxCacheSize; this.type = 'Cache'; /** * Options for the cache. */ this.options = { caseInsensitive: false }; this._registry = new BaseRegistry(); this._keyQueue = []; } /** * Gets the maximum size of the cache. */ get maxCacheSize() { return this._maxCacheSize; } /** * Generates a hash key based on the given namespace and value. * @param namespace The namespace for the key. * @param value The value for the key. * @returns The generated hash key. */ getHashKey(namespace, value) { const key = sha256(`${namespace}:${value}`); return key; } /** * Gets the value associated with the specified key. * @param key The key to retrieve the value for. * @returns The value associated with the key, or undefined if the key does not exist in the cache. */ get(key) { return this._registry.get(key); } /** * Sets the value for the specified key in the cache. * @param key The key to set the value for. * @param value The value to set. */ set(key, value) { const index = this._keyQueue.indexOf(key); if (index === -1) { this._keyQueue.push(key); } while (this._keyQueue.length > this._maxCacheSize) { const removeKey = this._keyQueue.shift(); if (removeKey) { this._registry.delete(removeKey); } } this._registry.set(key, value); } /** * Checks if the cache contains the specified key. * @param key The key to check. * @returns True if the cache contains the key, false otherwise. */ has(key) { return this._registry.has(key); } /** * Deletes the value associated with the specified key from the cache. * @param key The key to delete. * @returns True if the value was successfully deleted, false otherwise. */ delete(key) { const index = this._keyQueue.indexOf(key); if (index > -1) { this._keyQueue.splice(index, 1); } return this._registry.delete(key); } /** * Clears the cache, removing all key-value pairs. */ clear() { this._keyQueue.length = 0; this._registry.clear(); } /** * Gets the number of key-value pairs in the cache. */ get size() { return this._registry.size; } /** * Gets an iterator for the keys in the cache. */ get keys() { return this._registry.keys; } /** * Gets an iterator for the values in the cache. */ get values() { return this._registry.values; } /** * Gets an iterator for the entries (key-value pairs) in the cache. */ get entries() { return this._registry.entries; } /** * Executes a provided function once for each key-value pair in the cache. * @param callbackfn The function to execute for each key-value pair. * @param thisArg The value to use as `this` when executing the callback function. */ forEach(callbackfn, thisArg) { this._registry.forEach(callbackfn, thisArg); } /** * Returns an iterator for the entries (key-value pairs) in the cache. */ [Symbol.iterator]() { return this._registry[Symbol.iterator](); } } /** * Retrieves the key-value pair from an object based on the provided key. * @param obj - The object to search for the key-value pair. * @param key - The key to search for in the object. * @param caseInsesitive - Specifies whether the key comparison should be case-insensitive. * @returns The key-value pair as an array, or undefined if the key is not found. */ const getKeyValue = (obj, key, caseInsesitive) => { if (!caseInsesitive || typeof key !== 'string') { const value = obj[key]; return [key, value]; } if (typeof key === 'string') { let currentObj = obj; do { const keys = Object.getOwnPropertyNames(currentObj); if (Array.isArray(keys)) { const foundKey = keys.find(k => equalIgnoreCase(key, k)); if (foundKey) { const value = currentObj[foundKey]; return [foundKey, value]; } } } while ((currentObj = Object.getPrototypeOf(currentObj))); } return undefined; }; /** * Compares two values for equality, ignoring case sensitivity. * @param a - The first value to compare. * @param b - The second value to compare. * @returns `true` if the values are equal, `false` otherwise. */ const equalIgnoreCase = (a, b) => { if (typeof a === 'number' || typeof b === 'number') { return a === b; } else if (typeof a === 'symbol' || typeof b === 'symbol') { return a === b; } else if (typeof a === 'string' || typeof b === 'string') { return a.localeCompare(b, 'en', { sensitivity: 'base' }) === 0; } return undefined; }; /** * Retrieves the value from an object, ignoring the case of the key. * @param obj - The object to retrieve the value from. * @param key - The key to search for, ignoring the case. * @returns The value associated with the key, or undefined if the key is not found. */ const getValueIgnoreCase = (obj, key) => { const pair = getKeyValue(obj, key, true); if (pair) { return pair[1]; } return undefined; }; function isRegistryContext(context) { const value = ['Registry', 'CaseInsensitiveRegistry', 'BaseRegistry'].includes(context?.type); return value; } /** * Converts a context object to a Context instance. * @param context - The context object to convert. * @param options - Optional evaluation options. * @returns The converted Context instance. */ const fromContext = (context, options) => { const caseInsensitive = options?.caseInsensitive; if (!context) { return {}; } else if (context instanceof Registry) { return context; } else if (caseInsensitive && context instanceof Object) { return Registry.fromObject(context, options); } else { return context; } }; /** * Retrieves the value associated with the specified key from the given context. * If the context is an instance of Registry, it uses the `get` method to retrieve the value. * If the context is an object, it accesses the value using the key as a property name. * If the context is neither an instance of Registry nor an object, it returns undefined. * * @param context The context from which to retrieve the value. * @param key The key associated with the value to retrieve. * @returns The value associated with the specified key, or undefined if not found. */ const getContextValue = (context, key) => { if (context instanceof Registry) { return context.get(key); } else if (context instanceof Object) { return context[key]; } else { return undefined; } }; /** * Sets a value in the given context object. * If the context is an instance of Registry, the value is set using the key. * If the context is a plain object, the value is set using the key as a property name. * @param context - The context object. * @param key - The key or property name. * @param value - The value to be set. */ const setContextValue = (context, key, value) => { if (context instanceof Registry) { const registry = context; registry.set(key, value); } else if (context instanceof Object) { const object = context; object[key] = value; } }; /** * Retrieves the key from the given context object. * If the context is an instance of Registry, it searches for the key in a case-insensitive manner if specified. * If the context is an instance of Object, it uses the getKeyValue function to retrieve the key-value pair. * @param context - The context object. * @param key - The key to retrieve. * @param caseInsensitive - Specifies whether the search should be case-insensitive (only applicable for Registry instances). * @returns The retrieved key, or undefined if the key is not found. */ const getContextKey = (context, key, caseInsensitive) => { if (context instanceof Registry) { const keys = [...context.keys]; // : (string | number | symbol)[] const foundKey = caseInsensitive && typeof key === 'string' ? keys.find(k => equalIgnoreCase(key, k)) : keys.find(k => k === key); if (foundKey) { return foundKey; } } else if (context instanceof Object) { const pair = getKeyValue(context, key, caseInsensitive); if (pair) { return pair[0]; } } return undefined; }; /** * Represents the evaluation context for the code execution. */ class EvalContext { /** * Gets the original registry. */ get original() { return this._original; } /** * Gets the scopes registry. */ get priorScopes() { return this._priorScopes; } /** * Gets the scopes registry. */ get scopes() { return this._scopes; } /** * Gets the lookups registry. */ get lookups() { return this._lookups; } /** * Gets the evaluation options. */ get options() { return this._options; } /** * Creates a new instance of EvalContext. * @param original - The original registry or an object to create a registry from. * @param options - The evaluation options. */ constructor(original, options) { this.type = 'EvalContext'; this._original = original; this._priorScopes = []; this._scopes = new Stack(); this._options = options; this._lookups = []; } /** * Converts the given context to an instance of EvalContext. * @param context - The context to convert. * @param options - The evaluation options. * @returns The converted EvalContext instance. */ static fromContext(context, options) { if (context instanceof EvalContext) { return context; } const ctx = fromContext(context ?? {}, options); return new EvalContext(ctx, options ?? {}); } /** * Gets the value associated with the specified key from the evaluation context. * @param key - The key to retrieve the value for. * @returns The value associated with the key, or undefined if not found. */ get(key) { for (const scope of this._scopes.asArray()) { const value = getContextValue(scope, key); if (value !== undefined) { return value; } } if (this._original) { const value = getContextValue(this._original, key); if (value !== undefined) { return value; } } for (const scope of this._priorScopes) { const value = scope.get(key); if (value !== undefined) { return value; } } for (const lookup of this._lookups) { const value = lookup(key, this, this._options); if (value !== undefined) { return value; } } return undefined; } /** * Retrieves the `this` value of the specified key from the evaluation context. * The value is searched in the original context, prior scopes, and lookup functions. * @param key - The key to retrieve the value for. * @returns The `this` value associated with the key, or undefined if not found. */ getThis(key) { if (this._original) { const value = getContextValue(this._original, key); if (value !== undefined) { return this._original; } } for (const scope of this._priorScopes) { const value = getContextValue(this._original, key); if (value !== undefined) { return scope; } } for (const lookup of this._lookups) { const value = lookup(key, this, this._options); if (value !== undefined) { return lookup; } } return undefined; } /** * Retrieves the value associated with the specified key from the evaluation context. * * @param key - The key to retrieve the value for. * @returns The value associated with the key, or undefined if the key is not found. */ getKey(key) { const caseInsensitive = !!this._options.caseInsensitive; for (const scope of this._scopes.asArray()) { const foundKey = getContextKey(scope, key, caseInsensitive); if (foundKey !== undefined) { return foundKey; } } if (this._original) { const foundKey = getContextKey(this._original, key, caseInsensitive); if (foundKey !== undefined) { return foundKey; } } for (const scope of this._priorScopes) { const foundKey = getContextKey(scope.context, key, caseInsensitive); if (foundKey !== undefined) { return foundKey; } } return undefined; } /** * Sets a key-value pair in the context. * If the original context is a Registry, the key-value pair is set using the Registry's set method. * If the original context is an Object, the key-value pair is set directly on the object. * @param key - The key of the pair. * @param value - The value of the pair. */ set(key, value) { if (this._original && this._original instanceof Registry) { const registry = this._original; registry.set(key, value); } else if (this._original && this._original instanceof Object) { const obj = this._original; obj[key] = value; } } /** * Pushes a context to the scope stack. * @param context - The context to push. * @param options - The evaluation options. */ push(context, options) { const ctx = fromContext(context, options); this._scopes.push(ctx); } /** * Pops a context from the scope stack. */ pop() { this._scopes.pop(); } /** * Converts the EvalContext instance to an object representation. * If the original value is an instance of Registry, it converts it to an object using the toObject method of the registry. * If the original value is an object, it returns the original object. * @returns The object representation of the EvalContext instance. */ toObject() { if (this._original && this._original instanceof Registry) { const registry = this._original; const object = registry.toObject(); return object; } else if (this._original && this._original instanceof Object) { const obj = this._original; return obj; } return undefined; } } /** * Represents a trace of evaluation for a specific code execution. */ class EvalTrace extends Array { /** * Adds a new trace item to the evaluation trace. * @param node - The AST node associated with the trace item. * @param value - The value of the evaluated expression. * @param expression - The expression being evaluated. */ add(node, value, expression) { const item = { type: node.type, value }; if (expression) { item.expression = expression.substring(node.start, node.end); } this.push(item); } } /** * Represents the result of an evaluation. */ class EvalResult { /** * Gets the stack of evaluated values. */ get stack() { return this._stack; } /** * Gets the evaluated value. */ get value() { return this._value; } /** * Gets the error that occurred during evaluation. */ get error() { return this._error; } /** * Gets the error message associated with the evaluation error. */ get errorMessage() { return this._errorMessage; } /** * Gets whether the evaluation resulted in an error. */ get isError() { return this._isError; } /** * Gets whether the evaluation was successful. */ get isSuccess() { return this._isSuccess; } /** * Gets whether the evaluated value is undefined. */ get isUndefined() { return this._isUndefined; } /** * Gets the trace of evaluated values. */ get trace() { return this._trace; } /** * Gets the evaluation context. */ get context() { return this._context; } /** * Gets the evaluation options. */ get options() { return this._context.options; } /** * Gets the duration of the evaluation result. * @returns The duration in milliseconds. */ get duration() { if (!this._startDate || !this._endDate) { return undefined; } return this._endDate - this._startDate; } /** * Gets the expression associated with the evaluation result. * @returns The expression as a string, or undefined if no expression is associated. */ get expression() { return this._expression; } /** * Sets the expression for the evaluation result. * * @param value - The expression to be set. */ set expression(value) { this._expression = value; } /** * Creates a new instance of EvalResult. * @param trace The trace of evaluated values. * @param context The evaluation context. */ constructor(context) { this._stack = new Stack(); this._trace = new EvalTrace(); this._context = context; } /** * Starts the evaluation process. */ start() { this._startDate = performance.now(); } /** * Stops the evaluation and records the end date. */ stop() { this._endDate = performance.now(); return this.duration; } /** * Sets the success value of the evaluation result. * * @param value The value to set as the success value. * @returns void */ setSuccess(value) { this._value = value; this._isSuccess = true; this._isError = false; this._isUndefined = false; } /** * Sets the failure state of the evaluation result. * @param error The error object associated with the failure. */ setFailure(error) { this._error = error; if (error instanceof Error) { this._errorMessage = error.message; } this._isSuccess = false; this._isError = true; this._isUndefined = false; } } /** * Represents the evaluation state, which includes the context, result, and options. */ class EvalState { /** * Gets the evaluation context. */ get context() { return this._context; } /** * Gets the evaluation result. */ get result() { return this._result; } /** * Gets the evaluation options. */ get options() { return this._options; } /** * Gets whether the evaluation is asynchronous. */ get isAsync() { return this._isAsync; } /** * Represents the state of an evaluation. * @param context The evaluation context. * @param result The evaluation result. * @param options The evaluation options. * @param isAsync Indicates whether the evaluation is asynchronous. */ constructor(context, result, options, isAsync) { this._context = context; this._result = result; this._options = options; this._isAsync = isAsync; } /** * Creates an instance of EvalState from a given context, options, and async flag. * * @param context - The evaluation context or context object. * @param options - The evaluation options. * @param isAsync - A flag indicating whether the evaluation is asynchronous. * @returns The created EvalState instance. */ static fromContext(context, options, isAsync) { const ctx = EvalContext.fromContext(context, options); const result = new EvalResult(ctx); const state = new EvalState(ctx, result, options, isAsync); return state; } } const defaultParserOptions = { ...defaultOptions, ecmaVersion: 2020, extractExpressions: false, cacheSize: 100 }; const pushVisitorResult = (node, st, value) => { const result = st.result; result.stack.push(value); result.trace.add(node, value, st.result.expression); return value; }; const popVisitorResult = (node, st) => { const result = st.result; const value = result.stack.pop(); return value; }; const pushVisitorResultAsync = (node, st, value) => { const result = st.result; result.stack.push(value); result.trace.add(node, value, st.result.expression); return value; }; const popVisitorResultAsync = (node, st) => { const result = st.result; const value = result.stack.pop(); return value; }; const beforeVisitor = (node, st) => { const options = st.options; if (options?.['trackTime']) { const t = performance.now(); console.time('before: ' + node.type); return t; } return undefined; }; const afterVisitor = (node, st) => { const options = st.options; if (options?.['trackTime']) { const t = performance.now(); console.time('after: ' + node.type); return t; } return undefined; }; var RecursiveVisitorResultType; (function (RecursiveVisitorResultType) { RecursiveVisitorResultType[RecursiveVisitorResultType["Stack"] = 0] = "Stack"; RecursiveVisitorResultType[RecursiveVisitorResultType["Registry"] = 1] = "Registry"; })(RecursiveVisitorResultType || (RecursiveVisitorResultType = {})); const literals = Registry.fromObject({ 'undefined': undefined, 'null': null, 'true': true, 'false': false, }, { caseInsensitive: true }); const identifierVisitor = (node, st) => { if (st.options?.caseInsensitive) { return identifierVisitorCaseInsensitive(node, st); } beforeVisitor(node, st); const context = st.context; const value = node.name === 'this' ? st.context : context?.get(node.name); pushVisitorResult(node, st, value); afterVisitor(node, st); }; const identifierVisitorCaseInsensitive = (node, st) => { beforeVisitor(node, st); const context = st.context; const value = equalIgnoreCase(node.name, 'this') ? st.context : context?.get(node.name) ?? literals.get(node.name); pushVisitorResult(node, st, value); afterVisitor(node, st); }; const literalVisitor = (node, st) => { beforeVisitor(node, st); const value = node.value; pushVisitorResult(node, st, value); afterVisitor(node, st); }; // import { getCachedVisitorResult, setCachedVisitorResult } from './visitor-result-cache'; /** * Converts value to primitive for comparison operations */ const toPrimitive = (value, hint) => { if (value === null || value === undefined) return value; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'symbol' || typeof value === 'bigint') return value; // For objects, call valueOf/toString based on hint if (typeof value === 'object') { if (hint === 'number') { const valueOf = value.valueOf; if (typeof valueOf === 'function') { const result = valueOf.call(value); if (typeof result !== 'object') return result; } const toString = value.toString; if (typeof toString === 'function') { return toString.call(value); } } else { const toString = value.toString; if (typeof toString === 'function') { const result = toString.call(value); if (typeof result === 'string') return result; } const valueOf = value.valueOf; if (typeof valueOf === 'function') { const result = valueOf.call(value); if (typeof result !== 'object') return result; } } } return String(value); }; /** * Safely evaluates binary operations with proper type coercion */ const evaluateBinaryOperation = (operator, left, right) => { switch (operator) { case '+': { // Handle string concatenation and numeric addition according to JavaScript rules const leftPrim = toPrimitive(left); const rightPrim = toPrimitive(right); if (typeof leftPrim === 'string' || typeof rightPrim === 'string') { return String(leftPrim) + String(rightPrim); } return Number(leftPrim) + Number(rightPrim); } case '-': return Number(toPrimitive(left, 'number')) - Number(toPrimitive(right, 'number')); case '*': return Number(toPrimitive(left, 'number')) * Number(toPrimitive(right, 'number')); case '/': return Number(toPrimitive(left, 'number')) / Number(toPrimitive(right, 'number')); case '%': return Number(toPrimitive(left, 'number')) % Number(toPrimitive(right, 'number')); case '**': return Number(toPrimitive(left, 'number')) ** Number(toPrimitive(right, 'number')); // Bitwise operations - convert to 32-bit integers case '<<': return (Number(toPrimitive(left, 'number')) | 0) << (Number(toPrimitive(right, 'number')) | 0); case '>>': return (Number(toPrimitive(left, 'number')) | 0) >> (Number(toPrimitive(right, 'number')) | 0); case '>>>': return (Number(toPrimitive(left, 'number')) >>> 0) >>> (Number(toPrimitive(right, 'number')) | 0); case '&': return (Number(toPrimitive(left, 'number')) | 0) & (Number(toPrimitive(right, 'number')) | 0); case '^': return (Number(toPrimitive(left, 'number')) | 0) ^ (Number(toPrimitive(right, 'number')) | 0); case '|': return (Number(toPrimitive(left, 'number')) | 0) | (Number(toPrimitive(right, 'number')) | 0); // Comparison operations - follow JavaScript's abstract comparison rules case '==': return left == right; case '!=': return left != right; case '===': return left === right; case '!==': return left !== right; case '<': { const leftComp = toPrimitive(left, 'number'); const rightComp = toPrimitive(right, 'number'); // If both are strings, do string comparison if (typeof leftComp === 'string' && typeof rightComp === 'string') { return leftComp < rightComp; } return Number(leftComp) < Number(rightComp); } case '<=': { const leftLE = toPrimitive(left, 'number'); const rightLE = toPrimitive(right, 'number'); if (typeof leftLE === 'string' && typeof rightLE === 'string') { return leftLE <= rightLE; } return Number(leftLE) <= Number(rightLE); } case '>': { const leftGT = toPrimitive(left, 'number'); const rightGT = toPrimitive(right, 'number'); if (typeof leftGT === 'string' && typeof rightGT === 'string') { return leftGT > rightGT; } return Number(leftGT) > Number(rightGT); } case '>=': { const leftGE = toPrimitive(left, 'number'); const rightGE = toPrimitive(right, 'number'); if (typeof leftGE === 'string' && typeof rightGE === 'string') { return leftGE >= rightGE; } return Number(leftGE) >= Number(rightGE); } // Special operations case 'in': { if (typeof left === 'string' || typeof left === 'number' || typeof left === 'symbol') { if (right == null) { throw new Error(`Cannot use 'in' operator with null or undefined right operand`); } return left in right; } throw new Error(`Invalid left operand for 'in' operator: ${typeof left}`); } case 'instanceof': { if (typeof right === 'function') { return left instanceof right; } throw new Error(`Right operand of 'instanceof' is not a constructor`); } case '??': return left ?? right; default: throw new Error(`Unsupported binary operator: ${operator}`); } }; /** * Safely evaluates binary expressions with proper type handling */ const binaryExpressionVisitor = (node, st, callback) => { beforeVisitor(node, st); // Disable visitor result caching for now due to context sensitivity issues // const cachedResult = getCachedVisitorResult(node, st.context); // if (cachedResult !== undefined) { // pushVisitorResult(node, st, cachedResult); // afterVisitor(node, st); // return; // } callback(node.left, st); const left = popVisitorResult(node, st); callback(node.right, st); const right = popVisitorResult(node, st); const value = evaluateBinaryOperation(node.operator, left, right); // Disable visitor result caching for now // setCachedVisitorResult(node, st.context, value); pushVisitorResult(node, st, value); afterVisitor(node, st); }; /** * Prototype Pollution Prevention Utilities * * This module provides safeguards against prototype pollution attacks * by controlling access to dangerous property names and ensuring safe * object operations. */ /** * Set of property names that are dangerous for prototype pollution attacks */ const DANGEROUS_PROPERTY_NAMES = new Set([ '__proto__', 'constructor', 'prototype', '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toString', 'valueOf', 'toLocaleString' ]); /** * Set of constructor names that should be blocked from modification */ const DANGEROUS_CONSTRUCTORS$1 = new Set([ 'Object', 'Function', 'Array', 'String', 'Number', 'Boolean', 'Date', 'RegExp', 'Error', 'Promise' ]); /** * Checks if a property name is dangerous for prototype pollution * @param key The property name to check * @returns true if the property is dangerous, false otherwise */ const isDangerousProperty = (key) => { if (typeof key === 'string') { return DANGEROUS_PROPERTY_NAMES.has(key); } return false; }; /** * Checks if an object is a dangerous constructor that shouldn't be modified * @param obj The object to check * @returns true if the object is a dangerous constructor, false otherwise */ const isDangerousConstructor = (obj) => { if (typeof obj === 'function' && obj.name) { return DANGEROUS_CONSTRUCTORS$1.has(obj.name); } return false; }; /** * Safely sets a property on an object with prototype pollution protection * @param target The target object * @param key The property key * @param value The property value * @throws Error if the operation would cause prototype pollution */ const safeSetProperty = (target, key, value) => { if (!target || (typeof target !== 'object' && typeof target !== 'function')) { throw new Error(`Cannot set property on non-object: ${typeof target}`); } if (isDangerousProperty(key)) { throw new Error(`Access to dangerous property "${String(key)}" is blocked for security reasons`); } // Additional protection: don't allow setting properties on built-in prototypes if (isDangerousConstructor(target)) { throw new Error(`Modification of built-in constructor "${target.name}" is blocked for security reasons`); } // Check if we're trying to modify prototype chain if (target === Object.prototype || target === Function.prototype || target === Array.prototype || target === String.prototype || target === Number.prototype || target === Boolean.prototype) { throw new Error(`Modification of built-in prototype is blocked for security reasons`); } // Convert key to PropertyKey for safe operations let propertyKey; if (typeof key === 'string' || typeof key === 'number' || typeof key === 'symbol') { propertyKey = key; } else { // For other types, try to convert to string propertyKey = String(key); } // Use Object.defineProperty for safer assignment const descriptor = {