An expression evaluator for Angular.
Inspired by jse-eval, expression-eval and based on acorn, with thanks to their awesome work.
Many thanks to @Shelly and @donmccurdy for the initial packages.
JavaScript expression parsing and evaluation.
IMPORTANT: As mentioned under Security below, this library does not attempt to provide a secure sandbox for evaluation. Evaluation involving user inputs (expressions or values) may lead to unsafe behavior.
I wanted an evaluator to be included in one of my other projects. I found some great libraries, but they didn't have the features I needed and I found some issues with Angular integration. So I decided to create my own.
Evaluates an estree expression from acorn.
Install:
npm install --save @zvenigora/ng-eval-core
Import:
import { ParserService, EvalService,
CompilerService, DiscoveryService } from '@zvenigora/ng-eval-core';
import { ParserService } from '@zvenigora/ng-eval-core';
private service: ParserService;
...
const ast = service.parse('1 + foo');
The result of the parse is an AST (abstract syntax tree), like:
{
"type": "BinaryExpression",
"start": 0,
"end": 7,
"left": {
"type": "Literal",
"start": 0,
"end": 1,
"value": 1,
"raw": "1"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 4,
"end": 7,
"name": "foo"
}
}
Evaluation executes the AST using the given context simpleEval(ast, context)
. By default, the context is empty.
import { EvalService } from '@zvenigora/ng-eval-core';
private service: EvalService;
...
const expression = '2 + 3 * a';
const context = { a: 10 };
const result = service.simpleEval(expression, context); // 32
Since the default context is empty, it prevents using built-in JS functions.
To allow those functions, they can be added to the context
argument passed into the simpleEval
method:
const context = {
Date,
Array,
Object,
encodeURI,
decodeURI,
isFinite,
isNaN,
JSON,
Math,
parseFloat,
parseInt,
RegExp,
// ...myCustomPropertiesAndFunctions,
};
import { CompilerService } from '@zvenigora/ng-eval-core';
private service: CompilerService;
...
const fn = service.compile('x + y');
const context = { x: 1, y: 2 };
const value = service.simpleCall(fn, context, options); // 3
Ng-eval has a limited support for asynchronous expression based on JavaScript promises.
import { EvalService } from '@zvenigora/ng-eval-core';
private service: EvalService;
...
const context = {
one: 1,
two: 2,
asyncFunc: async (a: number, b: number) => { return await (a+b); }
};
const expr = 'asyncFunc(one, two)';
const value = await service.simpleEvalAsync(expr, context); // 3
import { CompilerService } from '@zvenigora/ng-eval-core';
private service: CompilerService;
...
const context = {
one: 1,
two: 2,
promiseFunc: (a: number, b: number) => {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(a + b);
}, 1000);
});
}
};
const expr = 'promiseFunc(one, two)';
const fn = service.compileAsync(expr);
const result = await service.simpleCallAsync(fn, context); // 3
Discovery service finds all nodes for the given node type.
import { DiscoveryService } from '@zvenigora/ng-eval-core';
private service: DiscoveryService;
...
const expressions = service.extract('1 + 2 * a', 'BinaryExpression'); // 2 nodes
The project has been tested with the following node types:
BinaryExpression
Identifier
Literal
CallExpression
potentially unsafeAwaitExpression
ConditionalExpression
MemberExpression
ArrayExpression
UnaryExpression
LogicalExpression
ThisExpression
NewExpression
TemplateLiteral
TaggedTemplateExpression
ObjectExpression
AssignmentExpression
UpdateExpression
ArrowFunctionExpression
potentially unsafe (AssignmentPattern is not implemented)
To change the default behavior of the evaluator, use options
. Options may be provided as an argument to the function call of simpleEval
.
While JavaScript is a case-sensitive language, some may find it hard to use. To provide case-insensitive evaluation, set caseInsensitive
to true
.
import { EvalService } from '@zvenigora/ng-eval-core';
private service: EvalService;
...
const options = {caseInsensitive: true};
...
const expression = '2 + 3 * A';
const context = { a: 10 };
const result = service.simpleEval(expression, context, options); // 32
Evaluation executes the AST using the given state eval(ast, state)
. The state
object includes the context, result, and options. It is in use by visitors functions behind the scene. It could be used to extend the functionality of the evaluator. For example, it can provide the execution history and the time of execution.
import { EvalService } from '@zvenigora/ng-eval-core';
private service: EvalService;
...
const expression = '2 + 3 * a';
const context = { a: 10 };
const options = { caseInsensitive: false };
const state = service.createState(context, options);
...
// update the state if required
...
const result = service.eval(expression, state); // 32
...
// read the state if required
console.table(state.result.trace); // display execution history
...
Evaluation context may include prior scopes. Evaluation scopes may contain variables and functions.
import { EvalService } from '@zvenigora/ng-eval-core';
private service: EvalService;
...
const args = ['says', 'meow'];
const cat = {
type: 'Cat',
name: 'Miss Kitty',
num: 3,
action: function(args: string[], n: number, t: string) {
return this.name + ' ' + args.join(' ') + ' ' + n + ' ' + t;
}
}
const catOptions: EvalScopeOptions = {
global: false,
caseInsensitive: false,
namespace: 'cat',
thisArg: cat
};
evalContext.priorScopes.push(EvalScope.fromObject(cat, catOptions));
const expression = 'cat.action(args, cat.num, "times")';
const result = service.simpleEval(expression, evalContext); // 'Miss Kitty says meow 3 times'
Depending on your specific use-case, there are other related packages available, including:
Although this package does avoid the use of eval()
, it cannot guarantee that user-provided expressions, or user-provided inputs to evaluation, will not modify the state or behavior of your application. This library does not attempt to provide a secure sandbox for evaluation. Evaluation of arbitrary user inputs (expressions or values) may lead to unsafe behavior. If your project requires a secure sandbox, consider alternatives.
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on the guidelines for contributing and then feel free to submit a PR with your contribution.
Help us keep this project open and inclusive. Please read and follow the Code of Conduct.
MIT License.