@qrvey/formula-lang
Version:
QFormula support for qrvey projects
353 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateAST = void 0;
const constants_1 = require("../constants");
const utils_1 = require("../utils");
const syntax_errors_1 = require("./syntax-errors");
const errors_1 = require("../errors");
const primitiveFunctions_1 = require("../utils/primitiveFunctions");
const formula_parser_1 = require("./formula-parser");
const qformula_grammar_1 = require("../grammar/qformula.grammar");
const isAggregate_1 = require("../utils/isAggregate");
const PARSER_VERSION = '0.0.0';
const LANG_NAME = 'QrveyLang';
function calculateAST(program, tree, context) {
var _a;
const cursor = tree.cursor();
const startNode = cursor.node;
if (!startNode)
return;
const syntaxErrors = (0, syntax_errors_1.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;
const primitive = (_a = body === null || body === void 0 ? void 0 : body.primitive) !== null && _a !== void 0 ? _a : constants_1.AST_PRIMITIVES.UNKNOWN;
const programScope = validateProgramScope(body !== null && body !== void 0 ? body : inference.flatBody[0], inference, primitive, context);
if (!programScope.isValid && programScope.error)
errors.unshift(programScope.error);
return {
type: constants_1.AST_TYPES.program,
exp: program,
lang: LANG_NAME,
version: PARSER_VERSION,
body,
from: startNode.from,
to: startNode.to,
errors,
variables,
formulaFunctions,
flatBody,
primitive,
};
}
exports.calculateAST = calculateAST;
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 = (0, formula_parser_1.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);
validateIfAggregateHasError(node.name, inference, resultNode, context);
return resultNode;
}
const VALID_OPERATIONS_BY_TYPE = {
[constants_1.AST_PRIMITIVES.NUMBER]: [
'+',
'-',
'*',
'/',
'<',
'<=',
'>',
'>=',
'=',
'<>',
],
[constants_1.AST_PRIMITIVES.STRING]: ['=', '<>'],
[constants_1.AST_PRIMITIVES.DATE]: ['=', '<>'],
[constants_1.AST_PRIMITIVES.BOOLEAN]: [],
[constants_1.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 === constants_1.AST_PRIMITIVES.NUMBER;
const rightIsAValidNumber = rightPrimitive === constants_1.AST_PRIMITIVES.NUMBER;
const operatorNode = (logicOperator !== null && logicOperator !== void 0 ? logicOperator : arithOperator);
const operator = (0, utils_1.getNodeValue)(program, operatorNode);
const operatorPosition = !operatorNode
? undefined
: (0, formula_parser_1.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 = constants_1.AST_PRIMITIVES.UNKNOWN;
if (logicOperator) {
if (operatorIsValidForOperation(operator, leftPrimitive, rightPrimitive)) {
primitive = constants_1.AST_PRIMITIVES.BOOLEAN;
}
}
else if (arithOperator && leftIsAValidNumber && rightIsAValidNumber) {
primitive = constants_1.AST_PRIMITIVES.NUMBER;
}
const hasInvalidError = !left ||
!right ||
!operatorNode ||
(arithOperator && !leftIsAValidNumber && !rightIsAValidNumber) ||
primitive === constants_1.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: constants_1.AST_TYPES.binaryExpression,
left,
right,
isValidAggregate: (left === null || left === void 0 ? void 0 : left.isValidAggregate) && (right === null || right === void 0 ? void 0 : right.isValidAggregate),
from: node.from,
to: node.to,
primitive,
validOperators,
};
if (hasInvalidError) {
inference.errorList.push(new errors_1.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 = (0, primitiveFunctions_1.primitiveIsIncluded)(right === null || right === void 0 ? void 0 : right.primitive, constants_1.AST_PRIMITIVES.NUMBER, constants_1.AST_PRIMITIVES.BOOLEAN)
? right.primitive
: constants_1.AST_PRIMITIVES.UNKNOWN;
const validOperators = (right === null || right === void 0 ? void 0 : right.primitive) === constants_1.AST_PRIMITIVES.NUMBER ? ['+', '-'] : [];
const unaryAST = {
operator: (0, utils_1.getNodeValue)(program, operator),
operatorNode: (0, formula_parser_1.createPositionASTFromSyntaxNode)(operator),
type: constants_1.AST_TYPES.unaryExpression,
right,
isValidAggregate: right === null || right === void 0 ? void 0 : right.isValidAggregate,
from: node.from,
to: node.to,
primitive,
validOperators,
};
if (!rightNode) {
inference.errorList.push(new errors_1.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 errors_1.MissingParenthesisError({
type: constants_1.AST_TYPES.parenthesisExpression,
from,
to,
primitive: (_a = resultNode === null || resultNode === void 0 ? void 0 : resultNode.primitive) !== null && _a !== void 0 ? _a : constants_1.AST_PRIMITIVES.UNKNOWN,
}));
}
return resultNode;
}
function createLiteralValue({ from, to }, dataType, value) {
return {
type: constants_1.AST_TYPES.literal,
isValidAggregate: true,
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 = (0, utils_1.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 = (0, utils_1.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 (!(0, utils_1.isValidDate)(dateValue))
return transformStringValue(program, node);
const value = dateValue.toISOString();
return createLiteralValue(node, constants_1.AST_PRIMITIVES.DATE, value);
}
function transformStringValue(program, node) {
const value = (0, utils_1.getNodeValue)(program, node).slice(1, -1);
return createLiteralValue(node, constants_1.AST_PRIMITIVES.STRING, value);
}
function transformNumberValue(program, node) {
const value = parseFloat((0, utils_1.getNodeValue)(program, node));
return createLiteralValue(node, constants_1.AST_PRIMITIVES.NUMBER, value);
}
function transformBooleanValue(program, node) {
const value = (0, utils_1.getNodeValue)(program, node);
return createLiteralValue(node, constants_1.AST_PRIMITIVES.BOOLEAN, value);
}
function transformVariableValue(program, node, inference, context) {
var _a, _b;
let variableValue = (0, utils_1.getNodeValue)(program, node).slice(1, -1);
const variableContext = (0, utils_1.getVariableContext)(variableValue, context);
const variableType = (0, utils_1.getVariableType)(variableContext);
if (variableType === constants_1.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: constants_1.AST_PRIMITIVES.STRING,
});
const baseResult = {
type: variableType,
value: variableValue,
isValidAggregate: !(0, isAggregate_1.isAggregateScope)(context),
context: variableContext,
from: node.from,
to: node.to,
primitive: (_a = variableContext === null || variableContext === void 0 ? void 0 : variableContext.type) !== null && _a !== void 0 ? _a : constants_1.AST_PRIMITIVES.UNKNOWN,
};
//IF externalFormula AND NOT sampleData, due to sampleData will replace the externalFormulas into a Literal (in Transpiler)
if (variableType === constants_1.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 errors_1.CircularDependencyError();
const parser = qformula_grammar_1.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;
}
function validateProgramScope(currentNode, inference, result, context) {
const isRaw = (0, isAggregate_1.isRawScope)(context);
if (isRaw) {
const hasSomeAggregated = inference.flatBody.some((item) => {
return (0, isAggregate_1.isAggregateFunction)(item);
});
return {
isValid: !hasSomeAggregated,
error: new errors_1.RowFunctionRequiredError(currentNode),
};
}
const isAggregated = (0, isAggregate_1.isAggregateScope)(context);
if (isAggregated && currentNode) {
let currentError;
const hasAnExpectedResult = (0, primitiveFunctions_1.primitiveIsIncluded)(result, ...constants_1.EXPECTED_AGGREGATE_RESULT);
if (!hasAnExpectedResult)
currentError = new errors_1.AggregatedPrimitiveMismatchError(currentNode);
const programIsAColumn = currentNode.type === constants_1.AST_TYPES.column ||
currentNode.type === constants_1.AST_TYPES.externalFormula;
if (programIsAColumn)
currentError = new errors_1.AggregatedFunctionRequiredError(currentNode);
return {
isValid: hasAnExpectedResult && !programIsAColumn,
error: currentError,
};
}
return {
isValid: false,
error: new errors_1.RowFunctionRequiredError(currentNode),
};
}
function validateIfAggregateHasError(nodeName, inference, resultNode, context) {
const hasError = constants_1.VALID_EXPRESSION_IN_AGGREGATE.includes(nodeName) &&
(0, isAggregate_1.isAggregateScope)(context) &&
resultNode &&
!resultNode.isValidAggregate;
if (hasError) {
inference.errorList.push(new errors_1.AggregatedFunctionRequiredError(resultNode));
}
}
//# sourceMappingURL=json-parser.js.map