UNPKG

@openmrs/esm-expression-evaluator

Version:

Utilities for evaluating user-defined expressions

177 lines (176 loc) 6.63 kB
/** @category Utility */ import { jsep } from "./evaluator.js"; import { globalsAsync } from "./globals.js"; /** * `extractVariableNames()` is a companion function for `evaluate()` and `evaluateAsync()` which extracts the * names of all unbound identifiers used in the expression. The idea is to be able to extract all of the names * of variables that will need to be supplied in order to correctly process the expression. * * @example * ```ts * // variables will be ['isEmpty', 'array'] * const variables = extractVariableNames('!isEmpty(array)') * ``` * * An identifier is considered "unbound" if it is not a reference to the property of an object, is not defined * as a parameter to an inline arrow function, and is not a global value. E.g., * * @example * ```ts * // variables will be ['obj'] * const variables = extractVariableNames('obj.prop()') * ``` * * @example * ```ts * // variables will be ['arr', 'needle'] * const variables = extractVariableNames('arr.filter(v => v === needle)') * ``` * * @example * ```ts * // variables will be ['myVar'] * const variables = extractVariableNames('new String(myVar)') * ``` * * Note that because this expression evaluator uses a restricted definition of "global" there are some Javascript * globals that will be reported as a unbound expression. This is expected because the evaluator will still fail * on these expressions. * * @param expression The expression to analyze, either as a string or pre-parsed expression. * @returns An array of variable names that are unbound in the expression. */ export function extractVariableNames(expression) { if (typeof expression !== 'string' && (typeof expression !== 'object' || !expression || !('type' in expression))) { throw `Unknown expression type ${expression}. Expressions must either be a string or pre-compiled string.`; } const context = createAsynchronousContext(); visitExpression(typeof expression === 'string' ? jsep(expression) : expression, context); return [ ...context.variables ]; } function visitExpression(expression, context) { switch(expression.type){ case 'UnaryExpression': return visitUnaryExpression(expression, context); case 'BinaryExpression': return visitBinaryExpression(expression, context); case 'ConditionalExpression': return visitConditionalExpression(expression, context); case 'CallExpression': return visitCallExpression(expression, context); case 'ArrowFunctionExpression': return visitArrowFunctionExpression(expression, context); case 'MemberExpression': return visitMemberExpression(expression, context); case 'ArrayExpression': return visitArrayExpression(expression, context); case 'SequenceExpression': return visitSequenceExpression(expression, context); case 'NewExpression': return visitNewExpression(expression, context); case 'Literal': return visitLiteral(expression, context); case 'Identifier': return visitIdentifier(expression, context); case 'TemplateLiteral': return visitTemplateLiteral(expression, context); case 'TemplateElement': return visitTemplateElement(expression, context); default: throw `Expression evaluator does not support expression of type '${expression.type}'`; } } function visitUnaryExpression(expression, context) { return visitExpression(expression.argument, context); } function visitBinaryExpression(expression, context) { const left = visitExpression(expression.left, context); const right = visitExpression(expression.right, context); return [ left, right ].filter(Boolean); } function visitConditionalExpression(expression, context) { const consequent = visitExpression(expression.consequent, context); const test = visitExpression(expression.test, context); const alternate = visitExpression(expression.alternate, context); return [ consequent, test, alternate ].filter(Boolean); } function visitCallExpression(expression, context) { const fn = visitExpression(expression.callee, context); expression.arguments?.map(handleNullableExpression(context)); return fn; } function visitArrowFunctionExpression(expression, context) { const newContext = { ...context }; newContext.isLocalExpression = true; const params = expression.params?.map(handleNullableExpression(newContext)) ?? []; const bodyVariables = visitExpression(expression.body, newContext) ?? []; if (bodyVariables && Array.isArray(bodyVariables)) { for (const v of bodyVariables){ if (!params.includes(v)) { context.variables.add(v); } } } } function visitMemberExpression(expression, context) { visitExpression(expression.object, context); const newContext = { ...context }; newContext.isLocalExpression = true; visitExpression(expression.property, newContext); } function visitArrayExpression(expression, context) { expression.elements?.map(handleNullableExpression(context)); } function visitSequenceExpression(expression, context) { expression.expressions?.map(handleNullableExpression(context)); } function visitNewExpression(expression, context) { expression.arguments?.map(handleNullableExpression(context)); } function visitTemplateLiteral(expression, context) { expression.expressions?.map(handleNullableExpression(context)); expression.quasis?.map(handleNullableExpression(context)); } function visitTemplateElement(expression, context) {} function visitIdentifier(expression, context) { if (!(expression.name in context.globals)) { if (!context.isLocalExpression) { context.variables.add(expression.name); } else { return expression.name; } } } function visitLiteral(expression, context) {} function createAsynchronousContext() { return createContextInternal(globalsAsync); } function createContextInternal(globals_) { const context = { globals: { ...globals_ }, isLocalExpression: false, variables: new Set() }; return context; } function handleNullableExpression(context) { return function handleNullableExpressionInner(expression) { if (expression === null) { return null; } return visitExpression(expression, context); }; }