PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
← All changes | libraries/vendor/PhpSpreadsheet/Calculation/Calculation.php +94 -22 3.3.1 → 3.4 View file →
@@ -89,8 +89,21 @@
89 89 * Calculation cache enabled.
90 90 */
91 91 private bool $calculationCacheEnabled = true;
92 92
93 + /**
94 + * Maximum number of entries in the formula token cache.
95 + * Default 0 (disabled). Set via setFormulaTokenCacheMaxSize() to enable.
96 + */
97 + private int $formulaTokenCacheMaxSize = 0;
98 +
99 + /**
100 + * Cache of parsed formula tokens, keyed by the raw formula string.
101 + *
102 + * @var array<string, array<mixed>|bool>
103 + */
104 + private array $formulaTokenCache = [];
105 +
93 106 private BranchPruner $branchPruner;
94 107
95 108 protected bool $branchPruningEnabled = true;
96 109
@@ -243,8 +256,9 @@
243 256 public function flushInstance(): void
244 257 {
245 258 $this->clearCalculationCache();
246 259 $this->branchPruner->clearBranchStore();
260 + $this->formulaTokenCache = [];
247 261 }
248 262
249 263 /**
250 264 * Get the Logger for this calculation engine instance.
@@ -369,8 +383,46 @@
369 383 $this->calculationCache = [];
370 384 }
371 385
372 386 /**
387 + * Clear the formula token cache.
388 + */
389 + public function clearFormulaTokenCache(): void
390 + {
391 + $this->formulaTokenCache = [];
392 + }
393 +
394 + /**
395 + * Get the current number of entries in the formula token cache.
396 + */
397 + public function getFormulaTokenCacheSize(): int
398 + {
399 + return count($this->formulaTokenCache);
400 + }
401 +
402 + /**
403 + * Set the maximum number of entries in the formula token cache.
404 + * Set to 0 to disable caching (default), or a positive integer to enable.
405 + */
406 + public function setFormulaTokenCacheMaxSize(int $size): self
407 + {
408 + $this->formulaTokenCacheMaxSize = max(0, $size);
409 + if ($this->formulaTokenCacheMaxSize === 0) {
410 + $this->formulaTokenCache = [];
411 + }
412 +
413 + return $this;
414 + }
415 +
416 + /**
417 + * Get the maximum number of entries allowed in the formula token cache.
418 + */
419 + public function getFormulaTokenCacheMaxSize(): int
420 + {
421 + return $this->formulaTokenCacheMaxSize;
422 + }
423 +
424 + /**
373 425 * Clear calculation cache for a specified worksheet.
374 426 */
375 427 public function clearCalculationCacheForWorksheet(string $worksheetName): void
376 428 {
@@ -513,9 +565,9 @@
513 565 fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
514 566 $value
515 567 );
516 568 }
517 - $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line
569 + $result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore argument.type ($value can be mixed not string)
518 570 if ($this->spreadsheet === null) {
519 571 throw new Exception('null spreadsheet in calculateCellValue');
520 572 }
521 573 $cellAddressAttempted = true;
@@ -533,9 +585,9 @@
533 585 if (!$cellAddressAttempted) {
534 586 $cellAddress = array_pop($this->cellStack);
535 587 }
536 588 if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
537 - $sheetName = $cellAddress['sheet'] ?? null;
589 + $sheetName = $cellAddress['sheet'];
538 590 $testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null;
539 591 if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
540 592 /** @var array{cell: string} $cellAddress */
541 593 $testSheet->getCell($cellAddress['cell']);
@@ -570,8 +622,14 @@
570 622 * @return array<mixed>|bool
571 623 */
572 624 public function parseFormula(string $formula)
573 625 {
626 + // Check the formula token cache first (only when caching is enabled)
627 + if ($this->formulaTokenCacheMaxSize > 0 && isset($this->formulaTokenCache[$formula])) {
628 + return $this->formulaTokenCache[$formula];
629 + }
630 +
631 + $originalFormula = $formula;
574 632 $formula = Preg::replaceCallback(
575 633 self::CALCULATION_REGEXP_CELLREF_SPILL,
576 634 fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
577 635 $formula
@@ -587,9 +645,23 @@
587 645 return [];
588 646 }
589 647
590 648 // Parse the formula and return the token stack
591 - return $this->internalParseFormula($formula);
649 + $result = $this->internalParseFormula($formula);
650 +
651 + // Cache the result when caching is enabled (clear cache if it exceeds the maximum size)
652 + if ($this->formulaTokenCacheMaxSize > 0) {
653 + // Phpstan says if condition is always false,
654 + // but coverage report says next statement is covered.
655 + if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) {
656 + $this->formulaTokenCache = [];
657 + }
658 + // Cache key is the original formula string (before ANCHORARRAY transformation)
659 + // to ensure consistent lookup regardless of internal transformations.
660 + $this->formulaTokenCache[$originalFormula] = $result;
661 + }
662 +
663 + return $result;
592 664 }
593 665
594 666 /**
595 667 * Calculate the value of a formula.
@@ -937,9 +1009,9 @@
937 1009 $returnMatrix = [];
938 1010 $pad = $rpad = ', ';
939 1011 foreach ($value as $row) {
940 1012 if (is_array($row)) {
941 - $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore-line
1013 + $returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore argument.type (array_map can theoretically return array<mixed> not array<string>)
942 1014 $rpad = '; ';
943 1015 } else {
944 1016 $returnMatrix[] = $this->showValue($row);
945 1017 }
@@ -1339,9 +1411,9 @@
1339 1411 } elseif ($isOperandOrFunction && !$expectingOperatorCopy) {
1340 1412 // do we now have a function/variable/number?
1341 1413 $expectingOperator = true;
1342 1414 $expectingOperand = false;
1343 - $val = $match[1] ?? ''; //* @phpstan-ignore-line
1415 + $val = $match[1] ?? '';
1344 1416 $length = strlen($val);
1345 1417
1346 1418 if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
1347 1419 // $val is known to be valid unicode from statement above, so Preg::replace is okay even with u modifier
@@ -1492,9 +1564,9 @@
1492 1564 $val = "{$rangeWS2}{$endRowColRef}{$val}";
1493 1565 } elseif (ctype_alpha($val) && strlen($val) <= 3) {
1494 1566 // Column range
1495 1567 $stackItemType = 'Column Reference';
1496 - $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
1568 + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow() : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
1497 1569 $val = "{$rangeWS2}{$val}{$endRowColRef}";
1498 1570 }
1499 1571 $stackItemReference = $val;
1500 1572 }
@@ -1518,9 +1590,9 @@
1518 1590 $stackItemType = 'Row Reference';
1519 1591 // unescape any apostrophes or double quotes in worksheet name
1520 1592 $val = str_replace(["''", '""'], ["'", '"'], $val);
1521 1593 $column = 'A';
1522 - if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
1594 + if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevop must be non-null), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
1523 1595 $column = $pCellParent->getHighestDataColumn($val);
1524 1596 }
1525 1597 $val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
1526 1598 $stackItemReference = $val;
@@ -1532,9 +1604,9 @@
1532 1604 $stackItemType = 'Column Reference';
1533 1605 // unescape any apostrophes or double quotes in worksheet name
1534 1606 $val = str_replace(["''", '""'], ["'", '"'], $val);
1535 1607 $row = '1';
1536 - if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
1608 + if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevOp must be non-null?), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
1537 1609 $row = $pCellParent->getHighestDataRow($val);
1538 1610 }
1539 1611 $val = "{$val}{$row}";
1540 1612 $stackItemReference = $val;
@@ -1684,9 +1756,9 @@
1684 1756 // Loop through each token in turn
1685 1757 foreach ($tokens as $tokenIdx => $tokenData) {
1686 1758 /** @var mixed[] $tokenData */
1687 1759 $this->processingAnchorArray = false;
1688 - if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line
1760 + if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore offsetAccess.nonOffsetAccessible ($tokens might be mixed not array)
1689 1761 $this->processingAnchorArray = true;
1690 1762 }
1691 1763 $token = $tokenData['value'];
1692 1764 // Branch pruning: skip useless resolutions
@@ -1786,9 +1858,9 @@
1786 1858 } else {
1787 1859 return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
1788 1860 }
1789 1861 }
1790 - } elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line
1862 + } elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore offsetAccess.invalidOffset ($token is mixed)
1791 1863 // if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack
1792 1864 // We must have two operands, error if we don't
1793 1865 $operand2Data = $stack->pop();
1794 1866 if ($operand2Data === null) {
@@ -1904,9 +1976,9 @@
1904 1976 }
1905 1977 if ($breakNeeded) {
1906 1978 break;
1907 1979 }
1908 - $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
1980 + $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
1909 1981 if ($pCellParent !== null && $this->spreadsheet !== null) {
1910 1982 $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
1911 1983 } else {
1912 1984 return $this->raiseFormulaError('Unable to access Cell Reference');
@@ -1975,9 +2047,9 @@
1975 2047 $result = $operand1;
1976 2048 } elseif (Information\ErrorValue::isError($operand2)) {
1977 2049 $result = $operand2;
1978 2050 } else {
1979 - $result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line
2051 + $result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore binaryOp.invalid (unwrapresult can return mixed rather than string)
1980 2052 $result = StringHelper::substring(
1981 2053 $result,
1982 2054 0,
1983 2055 DataType::MAX_STRING_LENGTH
@@ -2008,10 +2080,10 @@
2008 2080 if (count(Functions::flattenArray($cellIntersect)) === 0) {
2009 2081 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
2010 2082 $stack->push('Error', ExcelError::null(), null);
2011 2083 } else {
2012 - $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line
2013 - . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
2084 + $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
2085 + . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
2014 2086 $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
2015 2087 $stack->push('Value', $cellIntersect, $cellRef);
2016 2088 }
2017 2089
@@ -2024,15 +2096,15 @@
2024 2096 $stack->push('Value', $cellUnion, 'A1');
2025 2097
2026 2098 break;
2027 2099 }
2028 - } elseif (($token === '~') || ($token === '%')) {
2100 + } elseif (($token === '~') || ($token === '%')) { // @phpstan-ignore booleanOr.alwaysFalse (phpstan says token can't be plain string), identical.alwaysFalse (ditto), identical.alwaysFalse (ditto)
2029 2101 // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
2030 2102 if (($arg = $stack->pop()) === null) {
2031 2103 return $this->raiseFormulaError('Internal error - Operand value missing from stack');
2032 2104 }
2033 2105 $arg = $arg['value'];
2034 - if ($token === '~') {
2106 + if ($token === '~') { // @phpstan-ignore identical.alwaysFalse (phpstan says token can't be plain string)
2035 2107 $this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
2036 2108 $multiplier = -1;
2037 2109 } else {
2038 2110 $this->debugLog->writeDebugLog('Evaluating Percentile of %s', $this->showValue($arg));
@@ -2204,9 +2276,9 @@
2204 2276 $arg = $stack->pop();
2205 2277 $a = $argCount - $i - 1;
2206 2278 if (
2207 2279 ($passByReference)
2208 - && (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line
2280 + && (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore offsetAccess.nonOffsetAccessible (possibly need to pass 2 arguments to isset)
2209 2281 && ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
2210 2282 ) {
2211 2283 /** @var mixed[] $arg */
2212 2284 if ($arg['reference'] === null) {
@@ -2271,9 +2343,9 @@
2271 2343
2272 2344 if ($functionName !== 'MKMATRIX') {
2273 2345 if ($this->debugLog->getWriteDebugLog()) {
2274 2346 krsort($argArrayVals);
2275 - $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore-line
2347 + $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore argument.type (flattenArray returns array<mixed> rather than array<string>)
2276 2348 }
2277 2349 }
2278 2350
2279 2351 // Process the argument with the appropriate function call
@@ -2308,9 +2380,9 @@
2308 2380 }
2309 2381 }
2310 2382 } else {
2311 2383 // if the token is a number, boolean, string or an Excel error, push it onto the stack
2312 - /** @var ?string $token */
2384 + /** @var null|numeric-string $token */
2313 2385 if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) {
2314 2386 $excelConstant = strtoupper("$token");
2315 2387 $stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]);
2316 2388 if (isset($storeKey)) {
@@ -2316,9 +2388,9 @@
2316 2388 if (isset($storeKey)) {
2317 2389 $branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant];
2318 2390 }
2319 2391 $this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant]));
2320 - } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line
2392 + } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore function.alreadyNarrowedType (phpstan says is_bool has argument of *NEVER*?), equal.alwaysFalse (ditto)
2321 2393 /** @var array{type: string, reference: ?string} $tokenData */
2322 2394 $stack->push($tokenData['type'], $token, $tokenData['reference']);
2323 2395 if (isset($storeKey)) {
2324 2396 $branchStore[$storeKey] = $token;
@@ -2447,9 +2519,9 @@
2447 2519 /** @var array<string, mixed> $r */
2448 2520 $r = $stack->pop();
2449 2521 $result[$x] = $r['value'];
2450 2522 }
2451 - } elseif (is_array($operand2) && is_array($operand1)) {
2523 + } elseif (is_array($operand2) /*&& is_array($operand1)*/) {
2452 2524 // Operand 1 and Operand 2 are both arrays
2453 2525 if (!$recursingArrays) {
2454 2526 self::checkMatrixOperands($operand1, $operand2, 2);
2455 2527 }
@@ -2460,9 +2532,9 @@
2460 2532 $r = $stack->pop();
2461 2533 $result[$x] = $r['value'];
2462 2534 }
2463 2535 } else {
2464 - throw new Exception('Neither operand is an arra');
2536 + throw new Exception('Neither operand is an array');
2465 2537 }
2466 2538 // Log the result details
2467 2539 $this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
2468 2540 // And push the result onto the stack