@zvenigora/ng-eval-core
Version:
An expression evaluator for Angular
1,602 lines (1,580 loc) • 127 kB
JavaScript
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 = {