@qrvey/formula-lang
Version:
QFormula support for qrvey projects
299 lines • 13 kB
JavaScript
import { AST_PRIMITIVES, AST_TYPES } from '../constants';
import { getNodeValue, getMonthMaxDayAllowed, isValidDate, getVariableType, getVariableContext, } from '../utils';
import { calculateNodeSyntaxErrors } from './syntax-errors';
import { MissingParenthesisError, NotAllowedOperationError, CircularDependencyError, } from '../errors';
import { primitiveIsIncluded } from '../utils/primitiveFunctions';
import { createPositionASTFromSyntaxNode, transformFunctionExpression, } from './formula-parser';
import { QFormulaLang } from '../grammar/qformula.grammar';
const PARSER_VERSION = '0.0.0';
const LANG_NAME = 'QrveyLang';
export function calculateAST(program, tree, context) {
var _a;
const cursor = tree.cursor();
const startNode = cursor.node;
if (!startNode)
return;
const syntaxErrors = calculateNodeSyntaxErrors(program, startNode);
const inference = {
errorList: syntaxErrors,
formulaVariables: [],
formulaFunctions: [],
flatBody: [],
};
const body = transformNode(program, startNode, inference, context, cursor);
const { errorList: errors, formulaVariables: variables, formulaFunctions, flatBody, } = inference;
return {
type: AST_TYPES.program,
exp: program,
lang: LANG_NAME,
version: PARSER_VERSION,
body,
from: startNode.from,
to: startNode.to,
errors,
variables,
formulaFunctions,
flatBody,
primitive: (_a = body === null || body === void 0 ? void 0 : body.primitive) !== null && _a !== void 0 ? _a : AST_PRIMITIVES.UNKNOWN,
};
}
function transformNode(program, refNode, inference, context, cursor) {
const node = refNode !== null && refNode !== void 0 ? refNode : cursor === null || cursor === void 0 ? void 0 : cursor.node;
if (!node)
return undefined;
let resultNode;
switch (node.name) {
case 'BinaryExpression':
resultNode = transformBinaryExpression(program, node, inference, context);
break;
case 'UnaryExpression':
resultNode = transformUnaryExpression(program, node, inference, context);
break;
case 'ParenthesizedExpression':
resultNode = transformParenthesizedExpression(program, node, inference, context);
break;
case 'Function':
resultNode = transformFunctionExpression(program, node, inference, context, transformNode);
break;
case 'Number':
resultNode = transformNumberValue(program, node);
break;
case 'Date':
resultNode = transformDateValue(program, node);
break;
case 'String':
resultNode = transformStringValue(program, node);
break;
case 'Boolean':
resultNode = transformBooleanValue(program, node);
break;
case 'Variable':
resultNode = transformVariableValue(program, node, inference, context);
break;
default:
return (cursor === null || cursor === void 0 ? void 0 : cursor.next()) || node.firstChild
? transformNode(program, node.firstChild, inference, context, cursor)
: undefined;
}
if (resultNode !== undefined)
inference.flatBody.push(resultNode);
return resultNode;
}
const VALID_OPERATIONS_BY_TYPE = {
[AST_PRIMITIVES.NUMBER]: [
'+',
'-',
'*',
'/',
'<',
'<=',
'>',
'>=',
'=',
'<>',
],
[AST_PRIMITIVES.STRING]: ['=', '<>'],
[AST_PRIMITIVES.DATE]: ['=', '<>'],
[AST_PRIMITIVES.BOOLEAN]: [],
[AST_PRIMITIVES.UNKNOWN]: [],
};
function operatorIsValidForOperation(operator, leftPrimitive, rightPrimitive) {
const allowedOperators = calculateValidOperatorsByType(leftPrimitive, rightPrimitive);
return allowedOperators.includes(operator);
}
function calculateValidOperatorsByType(leftPrimitive, rightPrimitive) {
if (leftPrimitive === rightPrimitive && !Array.isArray(leftPrimitive)) {
return VALID_OPERATIONS_BY_TYPE[leftPrimitive];
}
return [];
}
function calculateBinaryElements(program, node, left, right) {
const leftPrimitive = left === null || left === void 0 ? void 0 : left.primitive;
const rightPrimitive = right === null || right === void 0 ? void 0 : right.primitive;
const logicOperator = node.getChild('LogicOp');
const arithOperator = node.getChild('ArithOp');
const leftIsAValidNumber = leftPrimitive === AST_PRIMITIVES.NUMBER;
const rightIsAValidNumber = rightPrimitive === AST_PRIMITIVES.NUMBER;
const operatorNode = (logicOperator !== null && logicOperator !== void 0 ? logicOperator : arithOperator);
const operator = getNodeValue(program, operatorNode);
const operatorPosition = !operatorNode
? undefined
: createPositionASTFromSyntaxNode(operatorNode);
const validOperators = calculateValidOperatorsByType(leftPrimitive, rightPrimitive);
// primitive is number if arithOperator is not null and both left and right are numbers
// primitive is boolean if logicOperator is not null and both left and right are booleans
let primitive = AST_PRIMITIVES.UNKNOWN;
if (logicOperator) {
if (operatorIsValidForOperation(operator, leftPrimitive, rightPrimitive)) {
primitive = AST_PRIMITIVES.BOOLEAN;
}
}
else if (arithOperator && leftIsAValidNumber && rightIsAValidNumber) {
primitive = AST_PRIMITIVES.NUMBER;
}
const hasInvalidError = !left ||
!right ||
!operatorNode ||
(arithOperator && !leftIsAValidNumber && !rightIsAValidNumber) ||
primitive === AST_PRIMITIVES.UNKNOWN;
return {
primitive,
operator,
operatorNode: operatorPosition,
hasInvalidError,
validOperators,
};
}
function transformBinaryExpression(program, node, inference, context) {
const [leftNode, rightNode] = node.getChildren('Expression');
const left = transformNode(program, leftNode, inference, context);
const right = transformNode(program, rightNode, inference, context);
const { primitive, operator, operatorNode, hasInvalidError, validOperators, } = calculateBinaryElements(program, node, left, right);
const binaryAST = {
operator,
operatorNode,
type: AST_TYPES.binaryExpression,
left,
right,
from: node.from,
to: node.to,
primitive,
validOperators,
};
if (hasInvalidError) {
inference.errorList.push(new NotAllowedOperationError(binaryAST));
}
return binaryAST;
}
function transformUnaryExpression(program, node, inference, context) {
const [rightNode] = node.getChildren('Expression');
const operator = node.getChild('ArithOp');
const right = transformNode(program, rightNode, inference, context);
const primitive = primitiveIsIncluded(right === null || right === void 0 ? void 0 : right.primitive, AST_PRIMITIVES.NUMBER, AST_PRIMITIVES.BOOLEAN)
? right.primitive
: AST_PRIMITIVES.UNKNOWN;
const validOperators = (right === null || right === void 0 ? void 0 : right.primitive) === AST_PRIMITIVES.NUMBER ? ['+', '-'] : [];
const unaryAST = {
operator: getNodeValue(program, operator),
operatorNode: createPositionASTFromSyntaxNode(operator),
type: AST_TYPES.unaryExpression,
right,
from: node.from,
to: node.to,
primitive,
validOperators,
};
if (!rightNode) {
inference.errorList.push(new NotAllowedOperationError(unaryAST));
}
return unaryAST;
}
function transformParenthesizedExpression(program, node, inference, context) {
var _a;
const expressionNode = node.getChild('Expression');
const parenthesisStart = node.getChild('ParenthesisStart');
const parenthesisEnd = node.getChild('ParenthesisEnd');
const resultNode = transformNode(program, expressionNode, inference, context);
if (!parenthesisStart || !parenthesisEnd) {
const [from, to] = [
!parenthesisStart ? node.from : parenthesisStart.from,
!parenthesisStart ? node.to : parenthesisStart.to,
];
inference.errorList.push(new MissingParenthesisError({
type: AST_TYPES.parenthesisExpression,
from,
to,
primitive: (_a = resultNode === null || resultNode === void 0 ? void 0 : resultNode.primitive) !== null && _a !== void 0 ? _a : AST_PRIMITIVES.UNKNOWN,
}));
}
return resultNode;
}
function createLiteralValue({ from, to }, dataType, value) {
return {
type: AST_TYPES.literal,
dataType,
value,
from,
to,
primitive: dataType,
};
}
// "MM/DD/YYYY" or "MM/DD/YYYY HH:mm:ss"
const DATE_REGEXP = /^(0?\d|1[012])\/([0-3]?\d)\/(\d{2}|\d{4})(?:\s+(\d|[01]\d|2[0-3]):([0-5]\d):([0-5]\d))?$/;
function transformDateValue(program, node) {
const dateStr = getNodeValue(program, node).slice(1, -1);
const dateMatch = DATE_REGEXP.exec(dateStr);
if (!dateMatch)
return transformStringValue(program, node);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_full, dateMonth, dateDay, dateYear, dateHour, dateMin, dateSec] = dateMatch;
const [yy, mm, dd, HH, MM, SS] = [
Number(dateYear),
Number(dateMonth) - 1,
Number(dateDay),
Number(dateHour !== null && dateHour !== void 0 ? dateHour : 0),
Number(dateMin !== null && dateMin !== void 0 ? dateMin : 0),
Number(dateSec !== null && dateSec !== void 0 ? dateSec : 0),
];
const monthMaxDayAllowed = getMonthMaxDayAllowed(mm, yy);
if (dd > monthMaxDayAllowed)
return transformStringValue(program, node);
const msValue = Date.UTC(yy, mm, dd, HH, MM, SS);
const dateValue = new Date(msValue);
if (!isValidDate(dateValue))
return transformStringValue(program, node);
const value = dateValue.toISOString();
return createLiteralValue(node, AST_PRIMITIVES.DATE, value);
}
function transformStringValue(program, node) {
const value = getNodeValue(program, node).slice(1, -1);
return createLiteralValue(node, AST_PRIMITIVES.STRING, value);
}
function transformNumberValue(program, node) {
const value = parseFloat(getNodeValue(program, node));
return createLiteralValue(node, AST_PRIMITIVES.NUMBER, value);
}
function transformBooleanValue(program, node) {
const value = getNodeValue(program, node);
return createLiteralValue(node, AST_PRIMITIVES.BOOLEAN, value);
}
function transformVariableValue(program, node, inference, context) {
var _a, _b;
let variableValue = getNodeValue(program, node).slice(1, -1);
const variableContext = getVariableContext(variableValue, context);
const variableType = getVariableType(variableContext);
if (variableType === AST_TYPES.token)
variableValue = `{{${variableValue}}}`;
const variableExistInInference = inference.formulaVariables
.map((v) => v.id)
.includes(variableValue);
if (!variableExistInInference)
inference.formulaVariables.push(variableContext !== null && variableContext !== void 0 ? variableContext : {
id: variableValue,
label: variableValue,
type: AST_PRIMITIVES.STRING,
});
const baseResult = {
type: variableType,
value: variableValue,
context: variableContext,
from: node.from,
to: node.to,
primitive: (_a = variableContext === null || variableContext === void 0 ? void 0 : variableContext.type) !== null && _a !== void 0 ? _a : AST_PRIMITIVES.UNKNOWN,
};
//IF externalFormula AND NOT sampleData, due to sampleData will replace the externalFormulas into a Literal (in Transpiler)
if (variableType === AST_TYPES.externalFormula && !(context === null || context === void 0 ? void 0 : context.useSampleData)) {
const isCircularDependency = (context === null || context === void 0 ? void 0 : context.currentFormulaId) &&
(context === null || context === void 0 ? void 0 : context.currentFormulaId) === variableValue;
if (isCircularDependency)
throw new CircularDependencyError();
const parser = QFormulaLang.parser;
const externalFunctionProgram = (_b = variableContext === null || variableContext === void 0 ? void 0 : variableContext.replacement) !== null && _b !== void 0 ? _b : '';
const tree = parser.parse(externalFunctionProgram);
const ast = calculateAST(externalFunctionProgram, tree, context);
const formulaBody = ast === null || ast === void 0 ? void 0 : ast.body;
return Object.assign(Object.assign({ isExternalFormula: true }, baseResult), formulaBody);
}
return baseResult;
}
//# sourceMappingURL=json-parser.js.map