Files
espocrm/application/Espo/Core/Formula/Parser.php
Yuri Kuznetsov 50db978fa2 cs fix
2021-04-21 13:57:18 +03:00

646 lines
20 KiB
PHP

<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Formula;
use Espo\Core\Formula\Exceptions\SyntaxError;
use StdClass;
/**
* Parses a formula script. Returns a RAW data object that represents a tree of functions.
*/
class Parser
{
private $priorityList = [
['='],
['||'],
['&&'],
['==', '!=', '>', '<', '>=', '<='],
['+', '-'],
['*', '/', '%'],
];
private $operatorMap = [
'=' => 'assign',
'||' => 'logical\\or',
'&&' => 'logical\\and',
'+' => 'numeric\\summation',
'-' => 'numeric\\subtraction',
'*' => 'numeric\\multiplication',
'/' => 'numeric\\division',
'%' => 'numeric\\modulo',
'==' => 'comparison\\equals',
'!=' => 'comparison\\notEquals',
'>' => 'comparison\\greaterThan',
'<' => 'comparison\\lessThan',
'>=' => 'comparison\\greaterThanOrEquals',
'<=' => 'comparison\\lessThanOrEquals',
];
public function parse(string $expression): StdClass
{
return $this->split($expression);
}
private function applyOperator(string $operator, string $firstPart, string $secondPart): StdClass
{
if ($operator === '=') {
if (strlen($firstPart)) {
if ($firstPart[0] == '$') {
return (object) [
'type' => 'assign',
'value' => [
(object) [
'type' => 'value',
'value' => substr($firstPart, 1)
],
$this->split($secondPart)
]
];
}
return (object) [
'type' => 'setAttribute',
'value' => [
(object) [
'type' => 'value',
'value' => $firstPart,
],
$this->split($secondPart)
]
];
}
throw new SyntaxError("Bad operator usage.");
}
$functionName = $this->operatorMap[$operator];
return (object) [
'type' => $functionName,
'value' => [
$this->split($firstPart),
$this->split($secondPart),
]
];
}
private function processStrings(
string &$string,
string &$modifiedString,
?array &$splitterIndexList = null,
bool $intoOneLine = false
): bool {
$isString = false;
$isSingleQuote = false;
$isComment = false;
$isLineComment = false;
$modifiedString = $string;
$braceCounter = 0;
for ($i = 0; $i < strlen($string); $i++) {
$isStringStart = false;
if (!$isLineComment && !$isComment) {
if ($string[$i] === "'" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isSingleQuote = true;
$isStringStart = true;
} else {
if ($isSingleQuote) {
$isString = false;
}
}
} else if ($string[$i] === "\"" && ($i === 0 || $string[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isStringStart = true;
$isSingleQuote = false;
} else {
if (!$isSingleQuote) {
$isString = false;
}
}
}
}
if ($isString) {
if ($string[$i] === '(' || $string[$i] === ')') {
$modifiedString[$i] = '_';
}
else if (!$isStringStart) {
$modifiedString[$i] = ' ';
}
}
else {
if (!$isLineComment && !$isComment) {
if (!$isComment) {
if ($i && $string[$i] === '/' && $string[$i - 1] === '/') {
$isLineComment = true;
}
}
if (!$isLineComment) {
if ($i && $string[$i] === '*' && $string[$i - 1] === '/') {
$isComment = true;
}
}
if ($string[$i] === '(') {
$braceCounter++;
}
if ($string[$i] === ')') {
$braceCounter--;
}
if ($braceCounter === 0) {
if (!is_null($splitterIndexList)) {
if ($string[$i] === ';') {
$splitterIndexList[] = $i;
}
}
if ($intoOneLine) {
if ($string[$i] === "\r" || $string[$i] === "\n" || $string[$i] === "\t") {
$string[$i] = ' ';
}
}
}
}
if ($isLineComment) {
if ($string[$i] === "\n") {
$isLineComment = false;
}
}
if ($isComment) {
if ($string[$i - 1] === "*" && $string[$i] === "/") {
$isComment = false;
}
}
}
}
return $isString;
}
private function split(string $expression): StdClass
{
$expression = trim($expression);
$braceCounter = 0;
$singleQuoteCounter = 0;
$hasExcessBraces = true;
$modifiedExpression = '';
$splitterIndexList = [];
$this->processStrings($expression, $modifiedExpression, $splitterIndexList, true);
$this->stripComments($expression, $modifiedExpression);
foreach ($splitterIndexList as $i => $index) {
if ($expression[$index] !== ';') {
unset($splitterIndexList[$i]);
}
}
$splitterIndexList = array_values($splitterIndexList);
$expressionOutOfBraceList = [];
for ($i = 0; $i < strlen($modifiedExpression); $i++) {
if ($modifiedExpression[$i] === '(') {
$braceCounter++;
}
if ($modifiedExpression[$i] === ')') {
$braceCounter--;
}
if ($braceCounter === 0 && $i < strlen($modifiedExpression) - 1) {
$hasExcessBraces = false;
}
if ($braceCounter === 0) {
$expressionOutOfBraceList[] = true;
} else {
$expressionOutOfBraceList[] = false;
}
}
if ($braceCounter !== 0) {
throw new SyntaxError('Incorrect round brackets in expression ' . $expression . '.');
}
if (
strlen($expression) > 1 &&
$expression[0] === '(' &&
$expression[strlen($expression) - 1] === ')' &&
$hasExcessBraces
) {
$expression = substr($expression, 1, strlen($expression) - 2);
return $this->split($expression);
}
if (count($splitterIndexList)) {
if ($expression[strlen($expression) - 1] !== ';') {
$splitterIndexList[] = strlen($expression);
}
$parsedPartList = [];
for ($i = 0; $i < count($splitterIndexList); $i++) {
if ($i > 0) {
$previousSplitterIndex = $splitterIndexList[$i - 1] + 1;
}
else {
$previousSplitterIndex = 0;
}
$part = trim(
substr(
$expression,
$previousSplitterIndex,
$splitterIndexList[$i] - $previousSplitterIndex
)
);
$parsedPartList[] = $this->parse($part);
}
return (object) [
'type' => 'bundle',
'value' => $parsedPartList,
];
}
$firstOperator = null;
$minIndex = null;
if ($expression === '') {
return (object) [
'type' => 'value',
'value' => null,
];
}
foreach ($this->priorityList as $operationList) {
foreach ($operationList as $operator) {
$startFrom = 1;
while (true) {
$index = strpos($expression, $operator, $startFrom);
if ($index === false) {
break;
}
if ($expressionOutOfBraceList[$index]) {
break;
}
$startFrom = $index + 1;
}
if ($index !== false) {
$possibleRightOperator = null;
if (strlen($operator) === 1) {
if ($index < strlen($expression) - 1) {
$possibleRightOperator = trim($operator . $expression[$index + 1]);
}
}
if (
$possibleRightOperator &&
$possibleRightOperator != $operator &&
!empty($this->operatorMap[$possibleRightOperator])
) {
continue;
}
$possibleLeftOperator = null;
if (strlen($operator) === 1) {
if ($index > 0) {
$possibleLeftOperator = trim($expression[$index - 1] . $operator);
}
}
if (
$possibleLeftOperator &&
$possibleLeftOperator != $operator &&
!empty($this->operatorMap[$possibleLeftOperator])
) {
continue;
}
$firstPart = substr($expression, 0, $index);
$secondPart = substr($expression, $index + strlen($operator));
$modifiedFirstPart = $modifiedSecondPart = '';
$isString = $this->processStrings($firstPart, $modifiedFirstPart);
$this->processStrings($secondPart, $modifiedSecondPart);
if (
substr_count($modifiedFirstPart, '(') === substr_count($modifiedFirstPart, ')')
&&
substr_count($modifiedSecondPart, '(') === substr_count($modifiedSecondPart, ')')
&&
!$isString
) {
if ($minIndex === null) {
$minIndex = $index;
$firstOperator = $operator;
}
else if ($index < $minIndex) {
$minIndex = $index;
$firstOperator = $operator;
}
}
}
}
if ($firstOperator) {
break;
}
}
if ($firstOperator) {
$firstPart = substr($expression, 0, $minIndex);
$secondPart = substr($expression, $minIndex + strlen($firstOperator));
$firstPart = trim($firstPart);
$secondPart = trim($secondPart);
return $this->applyOperator($firstOperator, $firstPart, $secondPart);
}
else {
$expression = trim($expression);
if ($expression[0] === '!') {
return (object) [
'type' => 'logical\\not',
'value' => $this->split(substr($expression, 1))
];
}
if ($expression[0] === '-' ) {
return (object) [
'type' => 'numeric\\subtraction',
'value' => [
$this->split('0'),
$this->split(substr($expression, 1))
]
];
}
if ($expression[0] === '+' ) {
return (object) [
'type' => 'numeric\\summation',
'value' => [
$this->split('0'),
$this->split(substr($expression, 1))
]
];
}
if (
$expression[0] === "'" && $expression[strlen($expression) - 1] === "'"
||
$expression[0] === "\"" && $expression[strlen($expression) - 1] === "\""
) {
return (object) [
'type' => 'value',
'value' => substr($expression, 1, strlen($expression) - 2)
];
}
if ($expression[0] === "$") {
return (object) [
'type' => 'variable',
'value' => substr($expression, 1)
];
}
if (is_numeric($expression)) {
$value = filter_var($expression, FILTER_VALIDATE_INT) !== false ?
(int) $expression :
(float) $expression;
return (object) [
'type' => 'value',
'value' => $value,
];
}
if ($expression === 'true') {
return (object) [
'type' => 'value',
'value' => true,
];
}
else if ($expression === 'false') {
return (object) [
'type' => 'value',
'value' => false,
];
}
else if ($expression === 'null') {
return (object) [
'type' => 'value',
'value' => null,
];
}
if ($expression[strlen($expression) - 1] === ')') {
$firstOpeningBraceIndex = strpos($expression, '(');
if ($firstOpeningBraceIndex > 0) {
$functionName = trim(substr($expression, 0, $firstOpeningBraceIndex));
$functionContent = substr($expression, $firstOpeningBraceIndex + 1, -1);
$argumentList = $this->parseArgumentListFromFunctionContent($functionContent);
$argumentSplitedList = [];
foreach ($argumentList as $argument) {
$argumentSplitedList[] = $this->split($argument);
}
return (object) [
'type' => $functionName,
'value' => $argumentSplitedList,
];
}
}
return (object) [
'type' => 'attribute',
'value' => $expression,
];
}
}
private function stripComments(string &$expression, string &$modifiedExpression): void
{
$commentIndexStart = null;
for ($i = 0; $i < strlen($modifiedExpression); $i++) {
if (is_null($commentIndexStart)) {
if (
$modifiedExpression[$i] === '/' &&
$i < strlen($modifiedExpression) - 1 &&
$modifiedExpression[$i + 1] === '/'
) {
$commentIndexStart = $i;
}
}
else {
if ($modifiedExpression[$i] === "\n" || $i === strlen($modifiedExpression) - 1) {
for ($j = $commentIndexStart; $j <= $i; $j++) {
$modifiedExpression[$j] = ' ';
$expression[$j] = ' ';
}
$commentIndexStart = null;
}
}
}
for ($i = 0; $i < strlen($modifiedExpression) - 1; $i++) {
if (is_null($commentIndexStart)) {
if ($modifiedExpression[$i] === '/' && $modifiedExpression[$i + 1] === '*') {
$commentIndexStart = $i;
}
}
else {
if ($modifiedExpression[$i] === '*' && $modifiedExpression[$i + 1] === '/') {
for ($j = $commentIndexStart; $j <= $i + 1; $j++) {
$modifiedExpression[$j] = ' ';
$expression[$j] = ' ';
}
$commentIndexStart = null;
}
}
}
}
private function parseArgumentListFromFunctionContent(string $functionContent): array
{
$functionContent = trim($functionContent);
$isString = false;
$isSingleQuote = false;
if ($functionContent === '') {
return [];
}
$commaIndexList = [];
$braceCounter = 0;
for ($i = 0; $i < strlen($functionContent); $i++) {
if ($functionContent[$i] === "'" && ($i === 0 || $functionContent[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isSingleQuote = true;
}
else {
if ($isSingleQuote) {
$isString = false;
}
}
}
else if ($functionContent[$i] === "\"" && ($i === 0 || $functionContent[$i - 1] !== "\\")) {
if (!$isString) {
$isString = true;
$isSingleQuote = false;
}
else {
if (!$isSingleQuote) {
$isString = false;
}
}
}
if (!$isString) {
if ($functionContent[$i] === '(') {
$braceCounter++;
}
else if ($functionContent[$i] === ')') {
$braceCounter--;
}
}
if ($braceCounter === 0 && !$isString && $functionContent[$i] === ',') {
$commaIndexList[] = $i;
}
}
$commaIndexList[] = strlen($functionContent);
$argumentList = [];
for ($i = 0; $i < count($commaIndexList); $i++) {
if ($i > 0) {
$previousCommaIndex = $commaIndexList[$i - 1] + 1;
}
else {
$previousCommaIndex = 0;
}
$argument = trim(
substr(
$functionContent,
$previousCommaIndex,
$commaIndexList[$i] - $previousCommaIndex
)
);
$argumentList[] = $argument;
}
return $argumentList;
}
}