UNPKG

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
/** * 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