eval-estree-expression
Version:
Safely evaluate JavaScript (estree) expressions, sync and async.
498 lines (397 loc) • 13.9 kB
JavaScript
'use strict';
const FunctionsSync = require('./FunctionsSync');
const Functions = require('./Functions');
const variables = require('./variables');
const utils = require('./utils');
const MAX_ARRAY_LENGTH = 10000;
const MAX_EXPRESSION_DEPTH = 50;
// const MAX_OBJECT_PROPERTIES = 1000;
// const MAX_STACK_DEPTH = 100;
class ExpressionSync {
static FAIL = Symbol('fail');
static ExpressionSync = ExpressionSync;
static variables = (tree, options) => variables(tree, options);
static evaluate = (tree, context, options) => {
return new ExpressionSync(tree, options).evaluate(context);
};
constructor(tree, options = {}, { created = false } = {}) {
this.options = {
maxExpressionDepth: MAX_EXPRESSION_DEPTH,
maxArrayLength: MAX_ARRAY_LENGTH,
...options
};
this.visitors = { ...this.options.visitors };
this.tree = tree;
this.state = {
stackDepth: 0,
expressionDepth: 0,
seenObjects: new WeakSet(),
templateLiterals: new Set()
};
this.stack = [];
this.seen = new Set();
if (options.functions && !created) {
const create = this.constructor.isAsync ? Functions : FunctionsSync;
const Expr = create(this.constructor);
return new Expr(tree, options, { created: true });
}
}
incrementDepth() {
this.state.expressionDepth++;
if (this.state.expressionDepth > MAX_EXPRESSION_DEPTH) {
throw new RangeError('Maximum expression depth exceeded');
}
}
decrementDepth() {
this.state.expressionDepth--;
}
visit(node, context, parent) {
this.incrementDepth();
try {
Reflect.defineProperty(node, 'parent', { value: node.parent || parent });
const visitor = this.visitors[node.type] || this[node.type];
if (typeof visitor !== 'function') {
const prefix = node.type === 'CallExpression' ? 'Functions are' : `visitor "${node.type}" is`;
const message = `${prefix} not supported`;
throw new TypeError(message);
}
const block = node.type === 'ArrayExpression' || node.type === 'ObjectExpression';
if (block) this.stack.push(node);
const value = visitor.call(this, node, context, parent);
const resolve = v => {
if (v instanceof Promise) return v.then(v => resolve(v));
if (block) this.stack.pop();
if (this.state.fail) return;
return v;
};
return resolve(value);
} finally {
this.decrementDepth();
}
}
evaluate(context) {
return this.visit(this.tree, context || {});
}
isRegExpOperator(node, context) {
return node.operator === '=' && node.right?.operator === '~' && this.options.regexOperator !== false;
}
assignment(node) {
switch (node.operator) {
case '=': // Assignment operator.
case '*=': // Multiplication assignment.
case '**=': // Exponentiation assignment.
case '/=': // Division assignment.
case '%=': // Remainder assignment.
case '+=': // Addition assignment.
case '-=': // Subtraction assignment
case '<<=': // Left shift assignment.
case '>>=': // Right shift assignment.
case '>>>=': // Unsigned right shift assignment.
case '&=': // Bitwise AND assignment.
case '^=': // Bitwise XOR assignment.
case '|=': // Bitwise OR assignment.
case '&&=': // Logical AND assignment.
case '||=': // Logical OR assignment.
case '??=': // Logical nullish assignment.
default: {
throw new SyntaxError(`Assignment expression "${node.operator}" is not supported`);
}
}
}
postfix(node, value) {
switch (node.operator) {
case '++': return value + 1;
case '--': return value - 1;
}
}
prefix(node, value) {
switch (node.operator) {
case '++': return value + 1;
case '--': return value - 1;
}
}
unset(node, context) {
const object = this.visit(node.object, context, node);
const unset = obj => {
if (utils.isObject(obj)) {
Reflect.deleteProperty(obj, node.property.name);
context[node.object.name] = obj;
return true;
}
};
return object instanceof Promise ? object.then(obj => unset(obj)) : unset(object);
}
comparison(left, operator, right, context, node) {
const bool = this.options.booleanLogicalOperators === true;
if (typeof left === 'symbol' || typeof right === 'symbol') {
this.state.fail = true;
return ExpressionSync.FAIL;
}
const s = v => {
if (typeof v === 'symbol') {
this.state.fail = true;
return;
}
return v;
};
// only lazily evaluate "right" when necessary
const val = () => s(typeof right === 'function' ? right() : right);
if (operator === 'in') {
const value = val();
if (!this.options.strict && (typeof value === 'string' || Array.isArray(value))) {
return value.includes(left);
}
return left in value;
}
switch (node.operator) {
// Arithmetic operators
case '+': return left + val();
case '-': return left - val();
case '/': return left / val();
case '*': return left * val();
case '%': return left % val();
case '**': return left ** val();
// Relational operators
case 'instanceof': return left instanceof val();
case '<': return left < val();
case '>': return left > val();
case '<=': return left <= val();
case '>=': return left >= val();
// Equality operators
case '!==': return left !== val();
case '===': return left === val();
case '!=': return left != val(); /* eslint-disable-line eqeqeq */
case '==': return left == val(); /* eslint-disable-line eqeqeq */
// Bitwise shift operators
case '<<': return left << val();
case '>>': return left >> val();
case '>>>': return left >>> val();
// Binary bitwise operators
case '&': return left & val();
case '|': return left | val();
case '^': return left ^ val();
// Binary logical operators
case '&&': return bool ? Boolean(left && val()) : left && val(); // Logical AND.
case '||': return bool ? Boolean(left || val()) : left || val(); // Logical OR.
case '??': return bool ? Boolean(left ?? val()) : left ?? val(); // Nullish Coalescing Operator.
}
}
/**
* Begin visitors
*/
AssignmentExpression(node, context, parent) {
if (this.isRegExpOperator(node, context)) {
const value = this.visit(node.left, context, node);
const regex = this.visit(node.right.argument, context, node.right);
if (regex instanceof RegExp) {
return regex.test(value);
}
}
try {
return this.assignment(node, context, parent);
} catch (err) {
throw new SyntaxError(`Assignment expression "${node.operator}" is not supported`);
}
}
ArrayExpression(node, context, parent) {
const array = [];
for (const child of node.elements) {
if (array.length >= this.options.maxArrayLength) {
throw new RangeError('Maximum array length exceeded');
}
const value = this.visit(child, context, node);
if (this.state.fail || value === ExpressionSync.FAIL) {
this.state.fail = true;
return ExpressionSync.FAIL;
}
if (child.type === 'SpreadElement') {
const spreadValues = value;
if (Array.isArray(spreadValues)) {
if (array.length + spreadValues.length > this.options.maxArrayLength) {
throw new RangeError('Maximum array length exceeded');
}
array.push(...spreadValues);
}
} else {
array.push(value);
}
}
return array;
}
BigIntLiteral(node) {
return BigInt(node.value);
}
BinaryExpression(node, context) {
const left = this.visit(node.left, context, node);
const right = () => this.visit(node.right, context, node);
if (left instanceof Promise) {
return left.then(v => this.comparison(v, node.operator, right, context, node));
}
return this.comparison(left, node.operator, right, context, node);
}
BlockStatement(node, context, parent) {
const output = [];
for (const child of node.body) {
output.push(this.visit(child, context, node));
}
return output;
}
BooleanLiteral(node) {
return node.value;
}
ConditionalExpression(node, context) {
const { test, consequent, alternate } = node;
const truthy = this.visit(test, context, node);
return truthy ? this.visit(consequent, context, node) : this.visit(alternate, context, node);
}
Identifier(node, context, parent) {
if (!utils.isSafeKey(node.name)) return;
if (context == null && this.options.strict !== false) {
throw new TypeError(`Cannot read property '${node.name}' of undefined`);
}
if (node.name === 'undefined' && this.stack.length === 0) {
return;
}
if (context != null) {
if (context[node.name] !== undefined) return context[node.name];
if (hasOwnProperty.call(context, node.name)) {
return;
}
}
const error = () => {
if (this.options.strict !== false) {
throw new ReferenceError(`${node.name} is undefined`);
}
this.state.fail = true;
return ExpressionSync.FAIL;
};
if (parent?.type === 'ObjectProperty' && parent.shorthand === true) {
return error();
}
if (this.stack.some(n => n.type === 'ArrayExpression' || n.type === 'ObjectExpression')) {
return error();
}
if (this.options.strict === true && this.options.functions !== true) {
throw new TypeError(`Cannot read property '${node.name}' of undefined`);
}
}
Literal(node) {
return node.value;
}
LogicalExpression(node, context, parent) {
return this.BinaryExpression(node, context, parent);
}
MemberExpression(node, context, parent) {
const { computed, object, property, unset } = node;
const value = this.visit(object, context, node) ?? context[object.name];
const data = computed ? context : value;
if (!utils.isSafeKey(property.name)) {
this.state.fail = true;
return ExpressionSync.FAIL;
}
let prop;
if (unset) {
prop = value;
} else if (node.optional) {
const optional = v => this.visit(property, data || {}, node);
prop = property instanceof Promise ? property.then(v => optional(v)) : optional(property);
} else {
prop = this.visit(property, data, node);
}
if (prop == null && property.name && data) {
prop = data[property.name];
}
if (!utils.isSafeKey(prop)) return;
return computed && value && prop != null ? value[prop] : prop;
}
NullLiteral(node) {
return null;
}
NumericLiteral(node) {
return node.value;
}
ObjectExpression(node, context) {
const object = {};
for (const property of node.properties) {
const { key, type, value } = property;
if (type === 'SpreadElement') {
Object.assign(object, this.visit(property, context, node));
} else {
const name = property.computed
? this.visit(key, context, property)
: (key.value || key.name);
object[name] = this.visit(value, context, property);
}
}
return object;
}
OptionalMemberExpression(node, context) {
const obj = this.visit(node.object, context, node);
const optional = v => this.visit(node.property, v || {}, node);
return obj instanceof Promise ? obj.then(v => optional(v)) : optional(obj);
}
RegExpLiteral(node) {
return new RegExp(node.pattern, node.flags);
}
SequenceExpression(node, context, parent) {
const length = node.expressions.length;
for (let i = 0; i < length - 1; i++) {
this.visit(node.expressions[i], context, node);
}
return this.visit(node.expressions[length - 1], context, node);
}
SpreadElement(node, context) {
return this.visit(node.argument, context, node);
}
StringLiteral(node, context) {
if (this.options.allowContextStringLiterals === true) {
return context[node.value] ?? node.value;
}
return node.value;
}
TemplateElement(node) {
return node.value.cooked;
}
TemplateLiteral(node, context) {
const length = node.expressions.length;
let output = '';
for (let i = 0; i < length; i++) {
output += this.visit(node.quasis[i], context, node);
output += this.visit(node.expressions[i], context, node);
}
output += this.visit(node.quasis[length], context, node);
return output;
}
ThisExpression(node, context) {
if (!context) throw new TypeError('Cannot read property "this" of undefined');
if (Reflect.has(context, 'this')) {
return context['this'];
}
}
UnaryExpression(node, context) {
const value = node.operator !== 'delete' && this.visit(node.argument, context, node);
const unary = v => {
switch (node.operator) {
case 'delete': return this.unset(node.argument, context, node);
case 'typeof': return typeof v;
case 'void': return void v;
case '~': return ~v;
case '!': return !v;
case '+': return +v; // eslint-disable-line no-implicit-coercion
case '-': return -v;
default: break;
}
};
return value instanceof Promise ? value.then(obj => unary(obj)) : unary(value);
}
UpdateExpression(node, context, parent) {
const value = this.visit(node.argument, context, node);
const update = v => {
const updated = node.prefix ? this.prefix(node, v) : this.postfix(node, v);
context[node.argument.name] = updated;
return updated;
};
return value instanceof Promise ? value.then(obj => update(obj)) : update(value);
}
}
module.exports = ExpressionSync;