{"record":{"id":"485bd26997cb8f60","repo":"PHPOffice/PhpSpreadsheet","slug":"syntax-error-currenttoken-lookahead-lookahead","errorCode":null,"errorMessage":"Syntax error: $currentToken, lookahead: $lookAhead, current char: $currentCharacter","messagePattern":"Syntax error: \\$currentToken, lookahead: \\$lookAhead, current char: \\$currentCharacter","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Writer\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Writer/Xls/Parser.php","lineNumber":1514,"sourceCode":"                . Calculation::CALCULATION_REGEXP_DEFINEDNAME\n                . '$/miu',\n                $this->currentToken\n            )\n            && $this->spreadsheet->getDefinedName($this->currentToken) !== null\n        ) {\n            $result = $this->createTree('ptgName', $this->currentToken, '');\n            $this->advance();\n\n            return $result;\n        }\n        if (Preg::isMatch('/^true|false$/i', $this->currentToken)) {\n            $result = $this->createTree($this->currentToken, '', '');\n            $this->advance();\n\n            return $result;\n        }\n\n        throw new WriterException('Syntax error: ' . $this->currentToken . ', lookahead: ' . $this->lookAhead . ', current char: ' . $this->currentCharacter);\n    }\n\n    /**\n     * It parses a function call. It assumes the following rule:\n     * Func -> ( Expr [,Expr]* ).\n     *\n     * @return mixed[] The parsed ptg'd tree on success\n     */\n    private function func(): array\n    {\n        $num_args = 0; // number of arguments received\n        $function = strtoupper($this->currentToken);\n        $result = ''; // initialize result\n        $this->advance();\n        $this->advance(); // eat the \"(\"\n        while ($this->currentToken !== ')') {\n            if ($num_args > 0) {\n                if ($this->currentToken === ',' || $this->currentToken === ';') {","sourceCodeStart":1496,"sourceCodeEnd":1532,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Writer/Xls/Parser.php#L1496-L1532","documentation":"The catch-all of the Xls formula parser's fact() step: after failing to match the current token as a parenthesis group, cell reference, number, string, function, boolean, or defined-name-shaped identifier, it throws 'Syntax error: {token}, lookahead: {lookAhead}, current char: {char}' exposing the exact parse position. It means the formula contains syntax the BIFF8 writer's grammar does not accept at all.","triggerScenarios":"Formulas using constructs outside the parser grammar: array constants ({1,2;3,4}), structured/table references (Table1[Amount]), the intersection (space) or union (,) operators, localized argument separators typed as text, or stray characters from string concatenation bugs.","commonSituations":"Copy-pasting modern Excel formulas into a system that exports legacy Xls; users pasting from localized Excel where list separators were ',' vs ';'; formula fragments joined with a missing operator ('=A1B2'); data imports that leave stray quotes/whitespace inside formulas.","solutions":["Use the diagnostic fields in the message: the token plus lookahead identifies the offending construct; search your cells for it.","Rewrite unsupported constructs (arrays -> cell ranges, table refs -> plain ranges).","Normalize localized separators and strip stray characters when accepting user formulas.","Save as Xlsx - its writer emits the formula string without this parse, so syntax the BIFF8 grammar rejects still round-trips."],"exampleFix":"// before\n$sheet->getCell('C3')->setValue('=SUM(Table1[Amount])'); // structured reference\n(new \\PhpOffice\\PhpSpreadsheet\\Writer\\Xls($spreadsheet))->save('out.xls');\n// Syntax error: Table1[Amount], lookahead: ...\n\n// after: plain range equivalent\n$sheet->getCell('C3')->setValue('=SUM(Sheet2!A2:A99)');\n// or keep the original formula and write Xlsx instead\n(new \\PhpOffice\\PhpSpreadsheet\\Writer\\Xlsx($spreadsheet))->save('out.xlsx');","handlingStrategy":"try-catch","validationCode":"/** Reject formula constructs the BIFF8 grammar cannot parse before saving Xls. */\nfunction formulaIsBiff8Safe(string $v): bool\n{\n    if ($v === '' || $v[0] !== '=') {\n        return true;\n    }\n    // structured references: Table1[Col], array constants: {1,2}, sheet-qualified refs handled elsewhere\n    if (preg_match('/\\w+\\[[^]]*\\]/', $v) || preg_match('/\\{[\\d.,\" ]+\\}/', $v)) {\n        return false;\n    }\n\n    return true;\n}\n\nforeach ($sheet->getCoordinates() as $coord) {\n    $v = $sheet->getCell($coord)->getValue();\n    if (is_string($v) && !formulaIsBiff8Safe($v)) {\n        throw new RuntimeException(\"Formula not supported by Xls writer at {$coord}: {$v}\");\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    (new \\PhpOffice\\PhpSpreadsheet\\Writer\\Xls($spreadsheet))->save($path);\n} catch (\\PhpOffice\\PhpSpreadsheet\\Writer\\Exception $e) {\n    if (str_starts_with($e->getMessage(), 'Syntax error:')) {\n        // message contains token + lookahead: find the cell, then fall back to Xlsx or report\n        $token = substr($e->getMessage(), strlen('Syntax error:'));\n        // ... locate and log offending cells, or:\n        (new \\PhpOffice\\PhpSpreadsheet\\Writer\\Xlsx($spreadsheet))->save(preg_replace('/\\.xls$/', '.xlsx', $path));\n    } else {\n        throw $e;\n    }\n}","preventionTips":["Constrain user-entered formulas to an allow-listed grammar (functions and reference shapes you have tested with the Xls writer).","Normalize localized separators (';' vs ',') and strip stray whitespace/quotes when importing formulas.","Add an integration test that saves your full representative template set as Xls on every release."],"tags":["xls","biff8","formula","parser","syntax-error","grammar"],"backgroundTag":"formula-parse-error","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}