{"record":{"id":"ae8530ce1cae5609","repo":"PHPOffice/PhpSpreadsheet","slug":"token-expected","errorCode":null,"errorMessage":"')' token expected.","messagePattern":"'\\)' token expected\\.","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Writer\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Writer/Xls/Parser.php","lineNumber":1397,"sourceCode":"\n    /**\n     * It parses a factor. It assumes the following rule:\n     * Fact -> ( Expr )\n     *       | CellRef\n     *       | CellRange\n     *       | Number\n     *       | Function.\n     *\n     * @return mixed[] The parsed ptg'd tree on success\n     */\n    private function fact(): array\n    {\n        $currentToken = $this->currentToken;\n        if ($currentToken === '(') {\n            $this->advance(); // eat the \"(\"\n            $result = $this->parenthesizedExpression();\n            if ($this->currentToken !== ')') {\n                throw new WriterException(\"')' token expected.\");\n            }\n            $this->advance(); // eat the \")\"\n\n            return $result;\n        }\n        // if it's a reference\n        if (Preg::isMatch('/^\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/', $this->currentToken)) {\n            $result = $this->createTree($this->currentToken, '', '');\n            $this->advance();\n\n            return $result;\n        }\n        if (\n            Preg::isMatch(\n                '/^'\n                . self::REGEX_SHEET_TITLE_UNQUOTED\n                . '(\\:' . self::REGEX_SHEET_TITLE_UNQUOTED\n                . ')?\\!\\$?[A-Ia-i]?[A-Za-z]\\$?\\d+$/u',","sourceCodeStart":1379,"sourceCodeEnd":1415,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Writer/Xls/Parser.php#L1379-L1415","documentation":"The Xls writer's recursive-descent formula parser consumes '(' then parses a parenthesized expression, and requires the next token to be ')'. An unbalanced parenthesis in the formula string - missing closer, or an extra opener later - makes currentToken !== ')' and throws \"')' token expected.\" with the parser positioned at the point of imbalance.","triggerScenarios":"Setting a cell to a malformed formula such as =SUM(A1:A5 (missing ')'), or programmatically concatenating formula fragments that drop a closing parenthesis, then saving as Xls; the parser tolerates the string in memory but must fully parse it at write time.","commonSituations":"User-typed formulas from an input field saved without validation; template string building ('=IF(' . $cond . ',' . $a . ',' . $b) forgetting the final ')'; Excel round-trips where a locale-specific export mangled parentheses.","solutions":["Validate parenthesis balance before setValue(): count '(' vs ')' or use a tokenizing check.","Build formulas with a tiny helper that appends and counts openers, or from a template engine that guarantees closure.","Catch WriterException at save and log the sheet/coordinate to locate the malformed cell quickly."],"exampleFix":"// before\n$sheet->getCell('B1')->setValue('=SUM(A1:A5'); // missing ')'\n(new \\PhpOffice\\PhpSpreadsheet\\Writer\\Xls($spreadsheet))->save('out.xls');\n// ')' token expected.\n\n// after: validate balance before setting\n$formula = '=SUM(A1:A5';\nif (substr_count($formula, '(') !== substr_count($formula, ')')) {\n    throw new InvalidArgumentException('Unbalanced parentheses: ' . $formula);\n}\n$sheet->getCell('B1')->setValue($formula . ')');","handlingStrategy":"validation","validationCode":"/** Cheap parenthesis-balance check for user-supplied formulas. */\nfunction formulaParensBalanced(string $formula): bool\n{\n    $depth = 0;\n    $inString = false;\n    $len = strlen($formula);\n    for ($i = 0; $i < $len; $i++) {\n        $ch = $formula[$i];\n        if ($ch === '\"') {\n            $inString = !$inString;\n        } elseif (!$inString) {\n            if ($ch === '(') {\n                ++$depth;\n            } elseif ($ch === ')') {\n                --$depth;\n                if ($depth < 0) {\n                    return false;\n                }\n            }\n        }\n    }\n\n    return $depth === 0 && !$inString;\n}\n\n$value = trim($userInput);\nif ($value !== '' && $value[0] === '=' && !formulaParensBalanced($value)) {\n    throw new InvalidArgumentException('Formula has unbalanced parentheses: ' . $value);\n}\n$sheet->getCell('B1')->setValue($value);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Validate any user-supplied formula (balance check at minimum) before setValue().","Build formulas via small composable helpers that pair each openParen with its close instead of string concatenation.","On save failure, log sheet title + coordinate so the one malformed cell among thousands is findable."],"tags":["xls","biff8","formula","parser","parentheses","syntax"],"backgroundTag":"unbalanced-parentheses","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}