kangaroo-expression
Version:
Secure expression evaluator with AST-based execution - A fast, safe, and powerful JavaScript-like expression language
1,203 lines (1,200 loc) • 112 kB
JavaScript
/**
* kangaroo-expression v0.0.3
* Secure expression evaluator with AST-based execution - A fast, safe, and powerful JavaScript-like expression language
*
* @author Flowbaker Team
* @license Apache-2.0
* @homepage https://github.com/xis/kangaroo
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('acorn')) :
typeof define === 'function' && define.amd ? define(['exports', 'acorn'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Kangaroo = {}, global.acorn));
})(this, (function (exports, acorn) { 'use strict';
class ASTParser {
constructor() {
this.defaultOptions = {
ecmaVersion: 2022,
sourceType: 'script',
locations: true,
allowReturnOutsideFunction: false,
preserveComments: false,
errorRecovery: false,
};
this.parseCache = new Map();
this.cacheSize = 0;
this.maxCacheSize = 1000;
}
parse(expression, options) {
if (!expression || typeof expression !== 'string') {
return null;
}
const trimmed = expression.trim();
if (!trimmed) {
return null;
}
const cacheKey = this.getCacheKey(trimmed, options);
if (this.parseCache.has(cacheKey)) {
return this.parseCache.get(cacheKey) || null;
}
try {
const result = this.parseInternal(trimmed, options);
this.setCached(cacheKey, result);
return result;
}
catch (error) {
this.setCached(cacheKey, null);
return null;
}
}
parseTemplate(template, options) {
const matches = this.extractTemplateExpressions(template);
return matches.map(match => ({
expression: match.expression,
parsed: this.parse(match.expression, options),
startIndex: match.startIndex,
endIndex: match.endIndex,
}));
}
static hasTemplateExpressions(text) {
return /\{\{[^{}]*\}\}/.test(text);
}
extractTemplateExpressions(template) {
const expressions = [];
const regex = /\{\{([^{}]*)\}\}/g;
let match;
while ((match = regex.exec(template)) !== null) {
const fullMatch = match[0];
const expression = match[1] ? match[1].trim() : '';
const startIndex = match.index;
const endIndex = match.index + fullMatch.length;
const multiline = expression.includes('\n');
if (expression) {
expressions.push({
fullMatch,
expression,
startIndex,
endIndex,
multiline,
});
}
}
return expressions;
}
static replaceTemplateExpressions(template, replacer) {
const parser = new ASTParser();
const matches = parser.extractTemplateExpressions(template);
let result = template;
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
if (match) {
const replacement = replacer(match.expression, match);
result = result.slice(0, match.startIndex) + replacement + result.slice(match.endIndex);
}
}
return result;
}
analyzeComplexity(expression) {
const parsed = this.parse(expression);
if (!parsed) {
return null;
}
const breakdown = {};
let functionCalls = 0;
let propertyAccesses = 0;
let maxDepth = 0;
this.walkAST(parsed.ast, (node, depth) => {
maxDepth = Math.max(maxDepth, depth || 0);
const nodeType = node.type;
breakdown[nodeType] = (breakdown[nodeType] || 0) + 1;
if (nodeType === 'CallExpression') {
functionCalls++;
}
else if (nodeType === 'MemberExpression') {
propertyAccesses++;
}
});
const estimatedTime = this.estimateExecutionTime(parsed.complexity);
const risk = this.assessRisk(parsed.complexity, maxDepth, functionCalls);
return {
score: parsed.complexity,
breakdown,
maxDepth,
functionCalls,
propertyAccesses,
estimatedTime,
risk,
};
}
clearCache() {
this.parseCache.clear();
this.cacheSize = 0;
}
getCacheStats() {
return {
size: this.cacheSize,
maxSize: this.maxCacheSize,
hitRate: 0,
};
}
parseInternal(expression, options) {
const parseOptions = { ...this.defaultOptions, ...options };
const wrappedExpression = `(${expression})`;
try {
const program = acorn.parse(wrappedExpression, parseOptions);
const ast = this.extractExpression(program);
if (!ast) {
return null;
}
const dependencies = this.extractDependencies(ast);
const functions = this.extractFunctionCalls(ast);
const complexity = this.calculateComplexity(ast);
const isSimple = this.isSimpleExpression(ast);
const hasTemplates = ASTParser.hasTemplateExpressions(expression);
const depth = this.calculateDepth(ast);
const estimatedMemoryUsage = this.estimateMemoryUsage(ast);
return {
ast,
dependencies,
functions,
complexity,
isSimple,
hasTemplates,
depth,
estimatedMemoryUsage,
};
}
catch (error) {
return null;
}
}
extractExpression(program) {
if (program.type !== 'Program') {
return null;
}
const body = program.body;
if (!Array.isArray(body) || body.length !== 1) {
return null;
}
const statement = body[0];
if (statement.type !== 'ExpressionStatement') {
return null;
}
return statement.expression;
}
extractDependencies(node) {
const dependencies = new Set();
this.walkAST(node, (currentNode) => {
if (currentNode.type === 'Identifier') {
const identifier = currentNode;
if (this.isContextVariable(identifier.name)) {
dependencies.add(identifier.name);
}
}
if (currentNode.type === 'MemberExpression') {
const memberExpr = currentNode;
if (memberExpr.object?.type === 'Identifier') {
const rootVar = memberExpr.object.name;
if (this.isContextVariable(rootVar)) {
dependencies.add(rootVar);
}
}
}
});
return dependencies;
}
extractFunctionCalls(node) {
const functions = new Set();
this.walkAST(node, (currentNode) => {
if (currentNode.type === 'CallExpression') {
const callExpr = currentNode;
if (callExpr.callee?.type === 'Identifier') {
functions.add(callExpr.callee.name);
}
if (callExpr.callee?.type === 'MemberExpression') {
const methodName = this.getMethodName(callExpr.callee);
const fullMethodName = this.getFullMethodName(callExpr.callee);
if (methodName) {
functions.add(methodName);
}
if (fullMethodName && fullMethodName !== methodName) {
functions.add(fullMethodName);
}
}
}
});
return functions;
}
calculateComplexity(node) {
let complexity = 0;
this.walkAST(node, (currentNode) => {
switch (currentNode.type) {
case 'CallExpression':
complexity += 3;
break;
case 'MemberExpression':
complexity += 1;
break;
case 'BinaryExpression':
case 'LogicalExpression':
complexity += 1;
break;
case 'ConditionalExpression':
complexity += 4;
break;
case 'ArrayExpression':
const arrayExpr = currentNode;
complexity += 2 + (arrayExpr.elements?.length || 0) * 0.5;
break;
case 'ObjectExpression':
const objExpr = currentNode;
complexity += 2 + (objExpr.properties?.length || 0) * 0.5;
break;
case 'ArrowFunctionExpression':
complexity += 5;
break;
default:
complexity += 0.5;
}
});
return Math.round(complexity * 10) / 10;
}
calculateDepth(node) {
let maxDepth = 0;
this.walkAST(node, (_, depth) => {
maxDepth = Math.max(maxDepth, depth || 0);
});
return maxDepth;
}
estimateMemoryUsage(node) {
let estimatedBytes = 0;
this.walkAST(node, (currentNode) => {
switch (currentNode.type) {
case 'Literal':
const literal = currentNode;
if (typeof literal.value === 'string') {
estimatedBytes += literal.value.length * 2;
}
else {
estimatedBytes += 8;
}
break;
case 'ArrayExpression':
estimatedBytes += 64;
break;
case 'ObjectExpression':
estimatedBytes += 128;
break;
case 'CallExpression':
estimatedBytes += 32;
break;
default:
estimatedBytes += 16;
}
});
return estimatedBytes;
}
isSimpleExpression(node) {
const allowedTypes = new Set([
'Identifier',
'MemberExpression',
'Literal',
'BinaryExpression',
'LogicalExpression'
]);
let isSimple = true;
let hasComplexOperation = false;
this.walkAST(node, (currentNode) => {
if (!allowedTypes.has(currentNode.type)) {
isSimple = false;
}
if (currentNode.type === 'CallExpression') {
hasComplexOperation = true;
isSimple = false;
}
if (currentNode.type === 'ConditionalExpression') {
hasComplexOperation = true;
isSimple = false;
}
});
return isSimple && !hasComplexOperation;
}
walkAST(node, visitor, depth = 0) {
visitor(node, depth);
for (const key in node) {
const value = node[key];
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
value.forEach(child => {
if (child && typeof child === 'object' && child.type) {
this.walkAST(child, visitor, depth + 1);
}
});
}
else if (value.type) {
this.walkAST(value, visitor, depth + 1);
}
}
}
}
isContextVariable(name) {
const contextVars = new Set([
'item', 'inputs', 'outputs', 'node', 'execution',
'true', 'false', 'null', 'undefined', 'Infinity', 'NaN'
]);
return contextVars.has(name);
}
getMethodName(memberExpression) {
if (!memberExpression.computed && memberExpression.property?.type === 'Identifier') {
return memberExpression.property.name;
}
return null;
}
getFullMethodName(memberExpression) {
if (!memberExpression.computed && memberExpression.property?.type === 'Identifier') {
const methodName = memberExpression.property.name;
if (memberExpression.object?.type === 'Identifier') {
const objectName = memberExpression.object.name;
const staticNamespaces = new Set([
'Object', 'Math', 'JSON', 'Date', 'Array', 'Crypto', 'String', 'Number'
]);
if (staticNamespaces.has(objectName)) {
return `${objectName}.${methodName}`;
}
}
}
return null;
}
estimateExecutionTime(complexity) {
return Math.max(0.1, complexity * 0.05);
}
assessRisk(complexity, depth, functionCalls) {
if (complexity > 50 || depth > 8 || functionCalls > 10) {
return 'high';
}
else if (complexity > 20 || depth > 5 || functionCalls > 5) {
return 'medium';
}
else {
return 'low';
}
}
getCacheKey(expression, options) {
const optionsStr = options ? JSON.stringify(options) : '';
return `${expression}|${optionsStr}`;
}
setCached(key, value) {
if (this.cacheSize >= this.maxCacheSize) {
const firstKey = this.parseCache.keys().next().value;
if (firstKey) {
this.parseCache.delete(firstKey);
this.cacheSize--;
}
}
this.parseCache.set(key, value);
this.cacheSize++;
}
}
class SecurityValidator {
constructor(functionRegistry) {
this.customRules = new Map();
this.validationCache = new Map();
this.cacheSize = 0;
this.maxCacheSize = 500;
this.blockedIdentifiers = new Set([
'eval', 'Function', 'constructor', 'prototype', '__proto__',
'window', 'document', 'global', 'globalThis', 'self', 'parent', 'top', 'frames',
'process', 'require', 'module', 'exports', '__dirname', '__filename',
'Buffer', 'setImmediate', 'clearImmediate', 'setInterval', 'clearInterval',
'alert', 'confirm', 'prompt', 'console', 'fetch', 'XMLHttpRequest',
'localStorage', 'sessionStorage', 'indexedDB', 'location', 'history', 'navigator',
'setTimeout', 'clearTimeout',
'Worker', 'SharedWorker', 'ServiceWorker', 'importScripts', 'import',
'WebAssembly',
'WebSocket', 'EventSource', 'FileReader', 'Blob', 'URL', 'URLSearchParams',
'postMessage', 'MessageChannel', 'BroadcastChannel',
'Error', 'SyntaxError', 'ReferenceError', 'TypeError',
]);
this.blockedProperties = new Set([
'constructor', 'prototype', '__proto__', '__defineGetter__', '__defineSetter__',
'__lookupGetter__', '__lookupSetter__', 'valueOf', 'toString',
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
'__defineProperty__', '__getOwnPropertyDescriptor__', '__getPrototypeOf__',
'__setPrototypeOf__', 'apply', 'call', 'bind',
]);
this.supportedNodeTypes = new Set([
'MemberExpression',
'CallExpression',
'Literal',
'Identifier',
'BinaryExpression',
'ConditionalExpression',
'ArrayExpression',
'ObjectExpression',
'Property',
'UnaryExpression',
'LogicalExpression',
'ArrowFunctionExpression',
'TemplateLiteral',
'TemplateElement',
]);
this.blockedOperators = new Set([
'instanceof', 'delete', 'new', 'typeof', 'void'
]);
this.dangerousPatterns = [
/javascript:/i,
/data:text\/html/i,
/data:application\/javascript/i,
/vbscript:/i,
/<script/i,
/on\w+\s*=/i,
/eval\s*\(/i,
/Function\s*\(/i,
/setTimeout\s*\(/i,
/setInterval\s*\(/i,
];
this.functionRegistry = functionRegistry;
this.initializeBuiltInRules();
}
validate(node, context) {
const cacheKey = this.getCacheKey(node, context);
if (this.validationCache.has(cacheKey)) {
return this.validationCache.get(cacheKey);
}
const startTime = Date.now();
const violations = [];
const rulesChecked = new Set();
this.validateNode(node, violations, context, rulesChecked);
const validationTime = Date.now() - startTime;
const riskLevel = this.assessRiskLevel(violations);
const result = {
isValid: violations.length === 0,
violations,
metadata: {
riskLevel,
rulesChecked: Array.from(rulesChecked),
validationTime,
},
};
this.setCached(cacheKey, result);
return result;
}
addRule(rule) {
this.customRules.set(rule.id, rule);
}
removeRule(ruleId) {
this.customRules.delete(ruleId);
}
getRules() {
return Array.from(this.customRules.values());
}
clearCache() {
this.validationCache.clear();
this.cacheSize = 0;
}
getCacheStats() {
return {
size: this.cacheSize,
maxSize: this.maxCacheSize,
};
}
validateNode(node, violations, context, rulesChecked) {
if (!this.supportedNodeTypes.has(node.type)) {
violations.push({
type: 'invalid_node_type',
message: `Node type '${node.type}' is not allowed`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only supported expression syntax',
});
return;
}
this.applyBuiltInValidation(node, violations, context, rulesChecked);
this.applyCustomRules(node, violations, context, rulesChecked);
this.validateChildren(node, violations, context, rulesChecked);
}
applyBuiltInValidation(node, violations, context, rulesChecked) {
switch (node.type) {
case 'Identifier':
this.validateIdentifier(node, violations);
rulesChecked?.add('identifier-validation');
break;
case 'MemberExpression':
this.validateMemberExpression(node, violations);
rulesChecked?.add('member-expression-validation');
break;
case 'CallExpression':
this.validateCallExpression(node, violations);
rulesChecked?.add('call-expression-validation');
break;
case 'BinaryExpression':
this.validateBinaryExpression(node, violations);
rulesChecked?.add('binary-expression-validation');
break;
case 'UnaryExpression':
this.validateUnaryExpression(node, violations);
rulesChecked?.add('unary-expression-validation');
break;
case 'ArrowFunctionExpression':
this.validateArrowFunction(node, violations);
rulesChecked?.add('arrow-function-validation');
break;
case 'Literal':
this.validateLiteral(node, violations);
rulesChecked?.add('literal-validation');
break;
case 'ObjectExpression':
this.validateObjectExpression(node, violations);
rulesChecked?.add('object-expression-validation');
break;
}
}
applyCustomRules(node, violations, context, rulesChecked) {
for (const rule of this.customRules.values()) {
try {
if (rule.check(node, context)) {
violations.push({
type: 'blocked_pattern',
message: rule.message,
node,
position: this.getPosition(node),
severity: rule.severity,
suggestion: rule.suggestion,
});
}
rulesChecked?.add(rule.id);
}
catch (error) {
console.warn(`Security rule '${rule.id}' failed:`, error);
}
}
}
validateIdentifier(node, violations) {
if (this.blockedIdentifiers.has(node.name)) {
violations.push({
type: 'blocked_identifier',
message: `Identifier '${node.name}' is blocked for security reasons`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only allowed identifiers and context variables',
});
}
}
validateMemberExpression(node, violations) {
if (!node.computed && node.property?.type === 'Identifier') {
if (this.blockedProperties.has(node.property.name)) {
violations.push({
type: 'blocked_property',
message: `Property '${node.property.name}' is blocked for security reasons`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Avoid accessing prototype chain properties',
});
}
}
if (this.isPrototypePollutionPattern(node)) {
violations.push({
type: 'blocked_pattern',
message: 'Potential prototype pollution pattern detected',
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use safe property access patterns',
});
}
const chainDepth = this.getPropertyChainDepth(node);
if (chainDepth > 10) {
violations.push({
type: 'depth_limit',
message: `Property chain too deep (${chainDepth} levels)`,
node,
position: this.getPosition(node),
severity: 'warning',
suggestion: 'Limit property chain depth to avoid performance issues',
});
}
}
validateCallExpression(node, violations) {
if (node.callee?.type === 'Identifier') {
const functionName = node.callee.name;
if (!this.functionRegistry.has(functionName)) {
violations.push({
type: 'blocked_identifier',
message: `Function '${functionName}' is not in the allowed function registry`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only registered safe functions',
});
}
}
if (node.callee?.type === 'MemberExpression') {
const methodName = this.getMethodName(node.callee);
const fullMethodName = this.getFullMethodName(node.callee);
const isCallbackMethod = methodName && ['filter', 'map', 'find', 'some', 'every', 'reduce'].includes(methodName);
if (isCallbackMethod) {
this.validateCallbackMethodCall(node, violations);
}
else {
const hasMethod = methodName && this.functionRegistry.has(methodName);
const hasFullMethod = fullMethodName && this.functionRegistry.has(fullMethodName);
if (!hasMethod && !hasFullMethod) {
violations.push({
type: 'blocked_identifier',
message: `Method '${fullMethodName || methodName}' is not in the allowed function registry`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only registered safe methods',
});
}
}
}
if (node.arguments && node.arguments.length > 20) {
violations.push({
type: 'complexity_limit',
message: `Too many function arguments (${node.arguments.length})`,
node,
position: this.getPosition(node),
severity: 'warning',
suggestion: 'Limit function arguments to reasonable numbers',
});
}
}
validateCallbackMethodCall(node, violations) {
if (!node.arguments || node.arguments.length === 0) {
violations.push({
type: 'blocked_pattern',
message: 'Callback methods require at least one argument',
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Provide a callback function for array methods',
});
return;
}
const firstArg = node.arguments[0];
if (firstArg.type !== 'ArrowFunctionExpression') {
violations.push({
type: 'blocked_pattern',
message: 'Callback methods require arrow function arguments',
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use arrow functions for array method callbacks',
});
}
}
validateBinaryExpression(node, violations) {
if (this.blockedOperators.has(node.operator)) {
violations.push({
type: 'blocked_pattern',
message: `Operator '${node.operator}' is not allowed`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only safe operators for comparisons and arithmetic',
});
}
}
validateUnaryExpression(node, violations) {
if (this.blockedOperators.has(node.operator)) {
violations.push({
type: 'blocked_pattern',
message: `Operator '${node.operator}' is not allowed`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Use only safe unary operators',
});
}
}
validateArrowFunction(node, violations) {
if (!Array.isArray(node.params)) {
violations.push({
type: 'invalid_node_type',
message: 'Arrow function parameters must be an array',
node,
position: this.getPosition(node),
severity: 'error',
});
return;
}
if (node.params.length > 4) {
violations.push({
type: 'complexity_limit',
message: `Arrow functions cannot have more than 4 parameters (got ${node.params.length})`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Limit arrow function parameters to (item, index, array)',
});
}
for (const param of node.params) {
if (param.type !== 'Identifier') {
violations.push({
type: 'invalid_node_type',
message: 'Arrow function parameters must be simple identifiers',
node,
position: this.getPosition(node),
severity: 'error',
});
continue;
}
if (this.blockedIdentifiers.has(param.name)) {
violations.push({
type: 'blocked_identifier',
message: `Parameter name '${param.name}' is blocked for security reasons`,
node,
position: this.getPosition(node),
severity: 'error',
});
}
}
}
validateLiteral(node, violations) {
if (typeof node.value === 'string') {
for (const pattern of this.dangerousPatterns) {
if (pattern.test(node.value)) {
violations.push({
type: 'blocked_pattern',
message: `String literal contains dangerous pattern: ${pattern.source}`,
node,
position: this.getPosition(node),
severity: 'error',
suggestion: 'Avoid script injection patterns in strings',
});
}
}
if (node.value.length > 10000) {
violations.push({
type: 'complexity_limit',
message: `String literal too long (${node.value.length} characters)`,
node,
position: this.getPosition(node),
severity: 'warning',
suggestion: 'Limit string length to reasonable sizes',
});
}
}
}
validateObjectExpression(node, violations) {
if (node.properties && node.properties.length > 50) {
violations.push({
type: 'complexity_limit',
message: `Object literal has too many properties (${node.properties.length})`,
node,
position: this.getPosition(node),
severity: 'warning',
suggestion: 'Limit object properties to reasonable numbers',
});
}
}
validateChildren(node, violations, context, rulesChecked) {
for (const key in node) {
const value = node[key];
if (value && typeof value === 'object') {
if (Array.isArray(value)) {
value.forEach(child => {
if (child && typeof child === 'object' && child.type) {
this.validateNode(child, violations, context, rulesChecked);
}
});
}
else if (value.type) {
this.validateNode(value, violations, context, rulesChecked);
}
}
}
}
isPrototypePollutionPattern(node) {
if (node.computed && node.property?.type === 'Literal') {
const propertyValue = node.property.value;
if (typeof propertyValue === 'string' && this.blockedProperties.has(propertyValue)) {
return true;
}
}
if (node.object?.type === 'MemberExpression') {
return this.isPrototypePollutionPattern(node.object);
}
return false;
}
getPropertyChainDepth(node) {
let depth = 1;
let current = node;
while (current.object?.type === 'MemberExpression') {
depth++;
current = current.object;
}
return depth;
}
getMethodName(memberExpression) {
if (!memberExpression.computed && memberExpression.property?.type === 'Identifier') {
return memberExpression.property.name;
}
return null;
}
getFullMethodName(memberExpression) {
if (!memberExpression.computed && memberExpression.property?.type === 'Identifier') {
const methodName = memberExpression.property.name;
if (memberExpression.object?.type === 'Identifier') {
const objectName = memberExpression.object.name;
const staticNamespaces = new Set([
'Object', 'Math', 'JSON', 'Date', 'Array', 'Crypto', 'String', 'Number'
]);
if (staticNamespaces.has(objectName)) {
return `${objectName}.${methodName}`;
}
}
}
return null;
}
getPosition(node) {
if (node.loc) {
return {
line: node.loc.start.line,
column: node.loc.start.column,
};
}
return undefined;
}
assessRiskLevel(violations) {
const errorCount = violations.filter(v => v.severity === 'error').length;
const warningCount = violations.filter(v => v.severity === 'warning').length;
if (errorCount > 0) {
return 'high';
}
else if (warningCount > 2) {
return 'medium';
}
else {
return 'low';
}
}
getCacheKey(node, context) {
const nodeKey = `${node.type}:${JSON.stringify(node).substring(0, 100)}`;
const contextKey = context ? JSON.stringify(context).substring(0, 50) : '';
return `${nodeKey}|${contextKey}`;
}
setCached(key, value) {
if (this.cacheSize >= this.maxCacheSize) {
const firstKey = this.validationCache.keys().next().value;
if (firstKey) {
this.validationCache.delete(firstKey);
this.cacheSize--;
}
}
this.validationCache.set(key, value);
this.cacheSize++;
}
initializeBuiltInRules() {
}
}
class ASTExecutor {
constructor(functionRegistry, options = {}) {
this.arrayOperations = null;
this.executionStack = [];
this.startTime = 0;
this.propertyCache = new Map();
this.cacheSize = 0;
this.maxCacheSize = 1000;
this.stats = {
totalExecutions: 0,
totalTime: 0,
cacheHits: 0,
cacheMisses: 0,
errors: 0,
};
this.functionRegistry = functionRegistry;
this.options = {
timeout: 5000,
maxStackDepth: 50,
collectMetrics: false,
enableDebugging: false,
...options,
};
}
setArrayOperations(arrayOperations) {
this.arrayOperations = arrayOperations;
}
execute(node, context = {}, options) {
const executeOptions = { ...this.options, ...options };
this.startTime = Date.now();
this.executionStack = [];
const metrics = executeOptions.collectMetrics ? {
parseTime: 0,
validationTime: 0,
executionTime: 0,
totalTime: 0,
} : undefined;
try {
this.stats.totalExecutions++;
this.checkTimeout(executeOptions.timeout || 5000);
const value = this.executeNode(node, context, executeOptions);
const executionTime = Date.now() - this.startTime;
this.stats.totalTime += executionTime;
if (metrics) {
metrics.executionTime = executionTime;
metrics.totalTime = executionTime;
}
return {
success: true,
value,
metadata: {
executionTime,
complexity: this.calculateNodeComplexity(node),
accessedVariables: this.extractAccessedVariables(node, context),
calledFunctions: this.extractCalledFunctions(node),
},
};
}
catch (error) {
this.stats.errors++;
const executionTime = Date.now() - this.startTime;
if (metrics) {
metrics.executionTime = executionTime;
metrics.totalTime = executionTime;
}
if (executeOptions.errorHandler) {
try {
const handledResult = executeOptions.errorHandler(error, node, context);
return {
success: true,
value: handledResult,
metadata: { executionTime },
};
}
catch (handlerError) {
}
}
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown execution error',
errorType: this.categorizeError(error),
metadata: { executionTime },
};
}
finally {
this.executionStack = [];
}
}
getStats() {
return { ...this.stats };
}
resetStats() {
this.stats = {
totalExecutions: 0,
totalTime: 0,
cacheHits: 0,
cacheMisses: 0,
errors: 0,
};
}
clearCache() {
this.propertyCache.clear();
this.cacheSize = 0;
}
executeNode(node, context, options) {
this.checkTimeout(options.timeout || 5000);
this.checkStackDepth(options.maxStackDepth || 50);
const stackFrame = {
node,
depth: this.executionStack.length,
startTime: Date.now(),
};
this.executionStack.push(stackFrame);
try {
switch (node.type) {
case 'Literal':
return this.executeLiteral(node);
case 'Identifier':
return this.executeIdentifier(node, context);
case 'MemberExpression':
return this.executeMemberExpression(node, context, options);
case 'CallExpression':
return this.executeCallExpression(node, context, options);
case 'BinaryExpression':
return this.executeBinaryExpression(node, context, options);
case 'LogicalExpression':
return this.executeLogicalExpression(node, context, options);
case 'ConditionalExpression':
return this.executeConditionalExpression(node, context, options);
case 'UnaryExpression':
return this.executeUnaryExpression(node, context, options);
case 'ArrayExpression':
return this.executeArrayExpression(node, context, options);
case 'ObjectExpression':
return this.executeObjectExpression(node, context, options);
default:
throw new Error(`Unsupported node type: ${node.type}`);
}
}
finally {
this.executionStack.pop();
}
}
executeLiteral(node) {
return node.value;
}
executeIdentifier(node, context) {
const name = node.name;
if (name in context) {
return context[name];
}
switch (name) {
case 'undefined':
return undefined;
case 'null':
return null;
case 'true':
return true;
case 'false':
return false;
case 'Infinity':
return Infinity;
case 'NaN':
return NaN;
default:
return undefined;
}
}
executeMemberExpression(node, context, options) {
const object = this.executeNode(node.object, context, options);
if (object == null) {
return undefined;
}
let property;
if (node.computed) {
property = this.executeNode(node.property, context, options);
}
else {
if (node.property.type !== 'Identifier') {
throw new Error('Invalid property access');
}
property = node.property.name;
}
return this.safePropertyAccess(object, property);
}
executeCallExpression(node, context, options) {
if (node.callee.type === 'Identifier') {
const args = node.arguments.map((arg) => this.executeNode(arg, context, options));
return this.executeFunction(node.callee.name, args, context);
}
if (node.callee.type === 'MemberExpression') {
const methodName = this.getMethodName(node.callee);
const fullMethodName = this.getFullMethodName(node.callee);
if (!methodName) {
throw new Error('Invalid method call');
}
if (fullMethodName && this.functionRegistry.has(fullMethodName)) {
const args = node.arguments.map((arg) => this.executeNode(arg, context, options));
return this.executeFunction(fullMethodName, args, context);
}
const object = this.executeNode(node.callee.object, context, options);
if (this.isCallbackMethod(methodName) && Array.isArray(object)) {
if (!this.arrayOperations) {
throw new Error('Array operations not initialized');
}
const originalArgs = node.arguments;
const evaluatedArgs = originalArgs.map((arg) => {
if (arg.type === 'ArrowFunctionExpression') {
return arg;
}
else {
return this.executeNode(arg, context, options);
}
});
return this.executeCallbackMethod(object, methodName, evaluatedArgs, context);
}
else {
const args = node.arguments.map((arg) => this.executeNode(arg, context, options));
return this.executeMethod(object, methodName, args);
}
}
throw new Error('Unsupported function call type');
}
executeBinaryExpression(node, context, options) {
const left = this.executeNode(node.left, context, options);
const right = this.executeNode(node.right, context, options);
switch (node.operator) {
case '+':
return left + right;
case '-':
return left - right;
case '*':
return left * right;
case '/':
return left / right;
case '%':
return left % right;
case '**':
return left ** right;
case '==':
return left == right;
case '!=':
return left != right;
case '===':
return left === right;
case '!==':
return left !== right;
case '<':
return left < right;
case '<=':
return left <= right;
case '>':
return left > right;
case '>=':
return left >= right;
case 'in':
return left in right;
default:
throw new Error(`Unsupported binary operator: ${node.operator}`);
}
}
executeLogicalExpression(node, context, options) {
const left = this.executeNode(node.left, context, options);
switch (node.operator) {
case '&&':
return left && this.executeNode(node.right, context, options);
case '||':
return left || this.executeNode(node.right, context, options);
case '??':
return left ?? this.executeNode(node.right, context, options);
default:
throw new Error(`Unsupported logical operator: ${node.operator}`);
}
}
executeConditionalExpression(node, context, options) {
const test = this.executeNode(node.test, context, options);
return test
? this.executeNode(node.consequent, context, options)
: this.executeNode(node.alternate, context, options);
}
executeUnaryExpression(node, context, options) {
const argument = this.executeNode(node.argument, context, options);
switch (node.operator) {
case '+':
return +argument;
case '-':
return -argument;
case '!':
return !argument;
case 'typeof':
return typeof argument;
case 'void':
return void argument;
default:
throw new Error(`Unsupported unary operator: ${node.operator}`);
}
}
executeArrayExpression(node, context, options) {
return node.elements.map((element) => element ? this.executeNode(element, context, options) : undefined);
}
executeObjectExpression(node, context, options) {
const result = {};
for (const property of node.properties) {
if (property.type !== 'Property') {
throw new Error('Unsupported ob