UNPKG

@qrvey/formula-lang

Version:

QFormula support for qrvey projects

299 lines 13 kB
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