@sensative/jsep-eval
Version:
A configurable eval function, which uses jsep to parse expressions. The generated result is evaluated w.r.t. a supplied object context.
169 lines (158 loc) • 5.71 kB
JavaScript
//
// evaluates javascript expression statements parsed with jsep
//
const _ = require('lodash');
const jsep = require('jsep');
const assert = require('assert');
const operators = {
binary: {
'===': (a, b) => (a === b),
'!==': (a, b) => (a !== b),
'==': (a, b) => (a == b), // eslint-disable-line
'!=': (a, b) => (a != b), // eslint-disable-line
'>': (a, b) => (a > b),
'<': (a, b) => (a < b),
'>=': (a, b) => (a >= b),
'<=': (a, b) => (a <= b),
'+': (a, b) => (a + b),
'-': (a, b) => (a - b),
'*': (a, b) => (a * b),
'/': (a, b) => (a / b),
'%': (a, b) => (a % b), // remainder
'**': (a, b) => (a ** b), // exponentiation
'&': (a, b) => (a & b), // bitwise AND
'|': (a, b) => (a | b), // bitwise OR
'^': (a, b) => (a ^ b), // bitwise XOR
'<<': (a, b) => (a << b), // left shift
'>>': (a, b) => (a >> b), // sign-propagating right shift
'>>>': (a, b) => (a >>> b), // zero-fill right shift
// Let's make a home for the logical operators here as well
'||': (a, b) => (a || b),
'&&': (a, b) => (a && b),
},
unary: {
'!': a => !a,
'~': a => ~a, // bitwise NOT
'+': a => +a, // unary plus
'-': a => -a, // unary negation
'++': a => ++a, // increment
'--': a => --a, // decrement
},
};
const types = {
// supported
LITERAL: 'Literal',
UNARY: 'UnaryExpression',
BINARY: 'BinaryExpression',
LOGICAL: 'LogicalExpression',
CONDITIONAL: 'ConditionalExpression', // a ? b : c
MEMBER: 'MemberExpression',
IDENTIFIER: 'Identifier',
THIS: 'ThisExpression', // e.g. 'this.willBeUsed'
CALL: 'CallExpression', // e.g. whatcha(doing)
ARRAY: 'ArrayExpression', // e.g. [a, 2, g(h), 'etc']
COMPOUND: 'Compound' // 'a===2, b===3' <-- multiple comma separated expressions.. returns last
};
const undefOperator = () => undefined;
const getParameterPath = (node, context) => {
assert(node, 'Node missing');
const type = node.type;
assert(_.includes(types, type), 'invalid node type');
assert(_.includes([types.MEMBER, types.IDENTIFIER], type), 'Invalid parameter path node type: ', type);
// the easy case: 'IDENTIFIER's
if (type === types.IDENTIFIER) {
return node.name;
}
// Otherwise it's a MEMBER expression
// EXAMPLES: a[b] (computed)
// a.b (not computed)
const computed = node.computed;
const object = node.object;
const property = node.property;
// object is either 'IDENTIFIER', 'MEMBER', or 'THIS'
assert(_.includes([types.MEMBER, types.IDENTIFIER, types.THIS], object.type), 'Invalid object type');
assert(property, 'Member expression property is missing');
let objectPath = '';
if (object.type === types.THIS) {
objectPath = '';
} else {
objectPath = node.name || getParameterPath(object, context);
}
if (computed) {
// if computed -> evaluate anew
const propertyPath = evaluateExpressionNode(property, context);
return objectPath + '[' + propertyPath + ']';
} else {
assert(_.includes([types.MEMBER, types.IDENTIFIER], property.type), 'Invalid object type');
const propertyPath = property.name || getParameterPath(property, context);
return (objectPath ? objectPath + '.': '') + propertyPath;
}
};
const evaluateExpressionNode = (node, context) => {
assert(node, 'Node missing');
assert(_.includes(types, node.type), 'invalid node type');
switch (node.type) {
case types.LITERAL: {
return node.value;
}
case types.THIS: {
return context;
}
case types.COMPOUND: {
const expressions = _.map(node.body, el => evaluateExpressionNode(el, context));
return expressions.pop();
}
case types.ARRAY: {
const elements = _.map(node.elements, el => evaluateExpressionNode(el, context));
return elements;
}
case types.UNARY: {
const operator = operators.unary[node.operator] || undefOperator;
assert(_.includes(operators.unary, operator), 'Invalid unary operator');
const argument = evaluateExpressionNode(node.argument, context);
return operator(argument);
}
case types.LOGICAL: // !!! fall-through to BINARY !!! //
case types.BINARY: {
const operator = operators.binary[node.operator] || undefOperator;
assert(_.includes(operators.binary, operator), 'Invalid binary operator');
const left = evaluateExpressionNode(node.left, context);
const right = evaluateExpressionNode(node.right, context);
return operator(left, right);
}
case types.CONDITIONAL: {
const test = evaluateExpressionNode(node.test, context);
const consequent = evaluateExpressionNode(node.consequent, context);
const alternate = evaluateExpressionNode(node.alternate, context);
return test ? consequent : alternate;
}
case types.CALL : {
assert(_.includes([types.MEMBER, types.IDENTIFIER, types.THIS], node.callee.type), 'Invalid function callee type');
const callee = evaluateExpressionNode(node.callee, context);
const args = _.map(node.arguments, arg => evaluateExpressionNode(arg, context));
return callee.apply(null, args);
}
case types.IDENTIFIER: // !!! fall-through to MEMBER !!! //
case types.MEMBER: {
const path = getParameterPath(node, context);
return _.get(context, path);
}
default:
return undefined;
}
};
const evaluate = (expression, context) => {
const tree = jsep(expression);
return evaluateExpressionNode(tree, context);
};
// is just a promise wrapper
const peval = (expression, context) => {
return Promise.resolve()
.then(() => evaluate(expression, context));
};
module.exports = {
evaluate,
peval,
types,
operators
};