@nlpjs/evaluator
Version:
Evaluator
566 lines (533 loc) • 16.4 kB
JavaScript
/*
* Copyright (c) AXA Group Operations Spain S.A.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const { generate: unparse } = require('escodegen');
const { parse } = require('esprima');
class JavascriptCompiler {
constructor(container) {
this.container = container.container || container;
this.name = 'javascript';
this.failResult = {};
}
compile(pipeline) {
const header = '(async () => {\n';
const footer = '\n})();';
const code = pipeline.join('\n');
return header + code + footer;
}
log(msg) {
const logger = this.container.get('logger') || console;
logger.info(msg);
}
walkLiteral(node) {
return node.value;
}
async walkUnary(node, context) {
switch (node.operator) {
case '+':
return +(await this.walk(node.argument, context));
case '-':
return -(await this.walk(node.argument, context));
case '~':
/* eslint-disable no-bitwise */
return ~(await this.walk(node.argument, context));
case '!':
return !(await this.walk(node.argument, context));
default:
return this.failResult;
}
}
async walkArray(node, context) {
const result = [];
for (let i = 0, l = node.elements.length; i < l; i += 1) {
const x = await this.walk(node.elements[i], context);
if (x === this.failResult) {
return this.failResult;
}
result.push(x);
}
return result;
}
async walkObject(node, context) {
const result = {};
for (let i = 0, l = node.properties.length; i < l; i += 1) {
const prop = node.properties[i];
const value = await this.walk(prop.value, context);
if (value === this.failResult) {
return this.failResult;
}
result[prop.key.value || prop.key.name] = value;
}
return result;
}
async walkBinary(node, context) {
const left = await this.walk(node.left, context);
if (left === this.failResult) {
return this.failResult;
}
if (node.operator === '&&' && !left) {
return false;
}
if (node.operator === '||' && left) {
return true;
}
const right = await this.walk(node.right, context);
if (right === this.failResult) {
return this.failResult;
}
switch (node.operator) {
case '==':
/* eslint-disable eqeqeq */
return left == right;
case '===':
return left === right;
case '!=':
/* eslint-disable eqeqeq */
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 '|':
/* eslint-disable no-bitwise */
return left | right;
case '&':
/* eslint-disable no-bitwise */
return left & right;
case '^':
/* eslint-disable no-bitwise */
return left ^ right;
case '||':
return left || right;
case '&&':
return left && right;
default:
return this.failResult;
}
}
async walkIdentifier(node, context) {
if ({}.hasOwnProperty.call(context, node.name)) {
return context[node.name];
}
if (context.input && {}.hasOwnProperty.call(context.input, node.name)) {
return context.input[node.name];
}
if (context.globalFuncs && context.globalFuncs[node.name]) {
const result = context.globalFuncs[node.name];
node.name = `globalFuncs.${node.name}`;
return result;
}
if (context.this && context.this.container) {
const item = context.this.container.get(node.name);
if (item) {
return item;
}
}
return undefined;
}
async walkThis(node, context) {
if ({}.hasOwnProperty.call(context, 'this')) {
// eslint-disable-next-line
return context["this"];
}
return undefined;
}
async walkCall(node, context) {
let callee;
if (
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === 'run'
) {
if (context && context.this && context.this.container) {
callee = context.this.container.runPipeline.bind(
context.this.container
);
} else {
return this.failResult;
}
} else {
callee = await this.walk(node.callee, context);
if (callee === this.failResult || typeof callee !== 'function') {
return this.failResult;
}
}
let ctx = node.callee.object
? await this.walk(node.callee.object, context)
: {};
if (ctx === this.failResult) {
ctx = null;
}
const args = [];
for (let i = 0, l = node.arguments.length; i < l; i += 1) {
const x = await this.walk(node.arguments[i], context);
if (x === this.failResult) {
return this.failResult;
}
args.push(x);
}
if (args.length === 0) {
args.push(context.input);
}
if (
node.callee &&
node.callee.type === 'Identifier' &&
node.callee.name === 'run'
) {
if (args.length === 1) {
args.push(context.input);
}
if (args.length === 2) {
args.push(context.this);
}
}
return callee.apply(ctx, args);
}
async walkMember(node, context) {
const obj = await this.walk(node.object, context);
if (obj === this.failResult || typeof obj === 'function') {
return this.failResult;
}
if (
node.property.type === 'Identifier' &&
node.object.type !== 'ObjectExpression'
) {
return obj[node.property.name];
}
const prop = await this.walk(node.property, context);
if (prop === this.failResult) {
return this.failResult;
}
return obj ? obj[prop] : undefined;
}
async walkConditional(node, context) {
const value = await this.walk(node.test, context);
if (value === this.failResult) {
return this.failResult;
}
if (value) {
return this.walk(node.consequent, context);
}
if (!node.alternate) {
return undefined;
}
return this.walk(node.alternate, context);
}
async walkExpression(node, context) {
const value = await this.walk(node.expression, context);
if (value === this.failResult) {
return this.failResult;
}
return value;
}
async walkReturn(node, context) {
return this.walk(node.argument, context);
}
async walkFunction(node, context) {
const newContext = {};
const keys = Object.keys(context).filter((x) => x !== 'this');
keys.forEach((element) => {
newContext[element] = context[element];
});
node.params.forEach((key) => {
if (key.type === 'Identifier') {
newContext[key.name] = null;
}
});
const bodies = node.body.body;
for (let i = 0, l = bodies.length; i < l; i += 1) {
if ((await this.walk(bodies[i], newContext)) === this.failResult) {
return this.failResult;
}
}
const vals = keys.map((key) => context[key]);
// eslint-disable-next-line
const result = Function(keys.join(', '), 'return ' + unparse(node)).apply(
null,
vals
);
return result;
}
async walkTemplateLiteral(node, context) {
let str = '';
for (let i = 0; i < node.expressions.length; i += 1) {
str += await this.walk(node.quasis[i], context);
str += await this.walk(node.expressions[i], context);
}
return str;
}
walkTemplateElement(node) {
return node.value.cooked;
}
async walkTaggedTemplate(node, context) {
const tag = await this.walk(node.tag, context);
const { quasi } = node;
const strings = [];
for (let i = 0; i < quasi.quasis.length; i += 1) {
const q = quasi.quasis[i];
const value = await this.walk(q, context);
strings.push(value);
}
const values = [];
for (let i = 0; i < quasi.expressions.length; i += 1) {
const q = quasi.expressions[i];
const value = await this.walk(q, context);
values.push(value);
}
// eslint-disable-next-line
return tag.apply(null, [strings].concat(values));
}
async walkUpdateExpression(node, context) {
let value = await this.walk(node.argument, context);
if (value === this.failResult) {
return this.failResult;
}
switch (node.operator) {
case '++':
value += 1;
return this.walkSet(node.argument, context, value);
case '--':
value -= 1;
return this.walkSet(node.argument, context, value);
default:
return this.failResult;
}
}
async walkVariableDeclaration(node, context) {
let value;
for (let i = 0; i < node.declarations.length; i += 1) {
const declaration = node.declarations[i];
value = declaration.init
? await this.walk(declaration.init, context)
: undefined;
if (value === this.failResult) {
return this.failResult;
}
await this.walkSet(declaration.id, context, value);
}
return value;
}
async walkAssignmentExpression(node, context) {
const value = await this.walk(node.right, context);
if (value === this.failResult) {
return this.failResult;
}
let leftValue = await this.walk(node.left, context);
if (leftValue === this.failResult) {
leftValue = 0;
}
switch (node.operator) {
case '=':
await this.walkSet(node.left, context, value);
return value;
case '+=':
leftValue += value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '-=':
leftValue -= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '*=':
leftValue *= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '/=':
leftValue /= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '%=':
leftValue %= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '|=':
// eslint-disable-next-line
leftValue |= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '&=':
// eslint-disable-next-line
leftValue &= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
case '^=':
// eslint-disable-next-line
leftValue ^= value;
await this.walkSet(node.left, context, leftValue);
return leftValue;
default:
return this.failResult;
}
}
async walkBlock(node, context) {
if (Array.isArray(node.body)) {
let result;
for (let i = 0; i < node.body.length; i += 1) {
result = await this.walk(node.body[i], context);
}
return result;
}
return this.walk(node.body, context);
}
async walk(node, context) {
switch (node.type) {
case 'Literal':
return this.walkLiteral(node, context);
case 'UnaryExpression':
return this.walkUnary(node, context);
case 'ArrayExpression':
return this.walkArray(node, context);
case 'ObjectExpression':
return this.walkObject(node, context);
case 'BinaryExpression':
case 'LogicalExpression':
return this.walkBinary(node, context);
case 'Identifier':
return this.walkIdentifier(node, context);
case 'ThisExpression':
return this.walkThis(node, context);
case 'CallExpression':
return this.walkCall(node, context);
case 'MemberExpression':
return this.walkMember(node, context);
case 'ConditionalExpression':
return this.walkConditional(node, context);
case 'ExpressionStatement':
return this.walkExpression(node, context);
case 'ReturnStatement':
return this.walkReturn(node, context);
case 'FunctionExpression':
return this.walkFunction(node, context);
case 'TemplateLiteral':
return this.walkTemplateLiteral(node, context);
case 'TemplateElement':
return this.walkTemplateElement(node, context);
case 'TaggedTemplateExpression':
return this.walkTaggedTemplate(node, context);
case 'UpdateExpression':
return this.walkUpdateExpression(node, context);
case 'AssignmentExpression':
return this.walkAssignmentExpression(node, context);
case 'IfStatement':
return this.walkConditional(node, context);
case 'BlockStatement':
return this.walkBlock(node, context);
case 'VariableDeclaration':
return this.walkVariableDeclaration(node, context);
default:
return this.failResult;
}
}
walkSetIdentifier(node, context, value) {
const newContext = context;
if ({}.hasOwnProperty.call(context, node.name)) {
context[node.name] = value;
} else if (
context.input &&
{}.hasOwnProperty.call(context.input, node.name)
) {
context.input[node.name] = value;
} else {
newContext[node.name] = value;
}
return value;
}
async walkSetMember(node, context, value) {
const obj = await this.walk(node.object, context);
if (obj === this.failResult || typeof obj === 'function') {
return this.failResult;
}
if (node.property.type === 'Identifier') {
obj[node.property.name] = value;
return value;
}
const prop = await this.walk(node.property, context);
if (prop === this.failResult) {
return this.failResult;
}
if (!obj) {
return this.failResult;
}
obj[prop] = value;
return value;
}
async walkSet(node, context, value) {
switch (node.type) {
case 'Identifier':
return this.walkSetIdentifier(node, context, value);
case 'MemberExpression':
return this.walkSetMember(node, context, value);
default:
return this.failResult;
}
}
async evaluateAll(str, context) {
const result = [];
const newContext = context || this.context;
const compiled = parse(str);
for (let i = 0; i < compiled.body.length; i += 1) {
let expression = compiled.body[i].expression
? compiled.body[i].expression
: compiled.body[i];
if (
expression.callee &&
expression.callee.type === 'ArrowFunctionExpression'
) {
expression = expression.callee.body;
}
const value = await this.walk(expression, newContext);
result.push(value === this.failResult ? undefined : value);
}
return result;
}
async evaluate(str, context) {
const result = await this.evaluateAll(str, context);
if (!result || result.length === 0) {
return undefined;
}
return result[result.length - 1];
}
async execute(compiled, srcInput, srcObject) {
const context = {
this: srcObject,
input: srcInput,
};
await this.evaluate(compiled, context);
}
}
module.exports = JavascriptCompiler;