Fix: parser rejects formulas with Excel's internal prefixes (#1655) - #1771
Open
marcin-kordas-hoc wants to merge 4 commits into
Open
marcin-kordas-hoc wants to merge 4 commits into
marcin-kordas-hoc wants to merge 4 commits into
Conversation
Excel marks every function added after the original OOXML specification with an
internal prefix when it serializes a workbook, so an imported file can contain
`_xlfn.IFS(...)` where the user typed `IFS(...)`. The ProcedureName token required
the name to start with a letter, so none of the prefixes matched and the formula
failed with "Parsing error. Redundant input, expecting EOF but found: (".
The prefixes are now an optional part of the ProcedureName and OffsetProcedureName
patterns, and are dropped when the token is turned into a function name. Doing this
in the lexer rather than by rewriting the formula string keeps the prefixes intact
wherever they are not a function name, such as inside a string literal.
FormulaParser and ParserWithCaching derive the name through one shared helper, so a
prefixed formula and its unprefixed equivalent also produce the same cache hash.
Closes #1655
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | a218523 | Commit Preview URL Branch Preview URL |
Sep 12 2026, 07:08 PM |
Performance comparison of head (a218523) vs base (c920375) |
marcin-kordas-hoc
marked this pull request as ready for review
September 12, 2026 06:42
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a697a2a. Configure here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
computeHashFromTokens only special-cased ProcedureName tokens; OffsetProcedureName is a separate, uncategorized token type (OFFSET has its own grammar rule), so it fell through to the generic branch that hashes the raw token image — meaning _xlfn.OFFSET(...) and OFFSET(...) did NOT share a cache entry, contradicting this fix's own stated design goal. Verified empirically before and after (hashes differ pre-fix, match post-fix) using the parser's own tokenizeFormula(), not a separately built lexer — a second buildLexerConfig() call creates a distinct OffsetProcedureName token-type object, so testing this via an external lexer silently never exercises the real bug (fixed the same latent issue in the existing 'shares one cache entry' spec test). Also adds a cheap leading-underscore guard to canonicalProcedureNameFromToken/ canonicalOffsetProcedureNameFromToken before invoking the prefix-stripping regex, since every prefix starts with '_' and the overwhelming majority of tokens don't have one. Verified as a real, correctness-preserving improvement for the common case with a standalone microbenchmark; a reorder alternative suggested by review was tried and measured to NOT help (and slightly hurt the prefixed case), so it was not adopted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- parser-consts.ts / LexerConfig.ts: the 'prefixes are matched in lower case only'
claim is true for ProcedureName but not for OffsetProcedureName, which reuses the
prefix pattern inside an already case-insensitive regex (the 'i' flag exists for
the translated OFFSET name, pre-dating this fix). Replaced 'harmless' with the
actual asymmetry and why it's not worth a dedicated case-sensitive-prefix matcher:
OFFSET predates the OOXML cutoff this prefix scheme exists for, so Excel can never
emit a prefixed OFFSET call in any case — there is no reachable real input, only
hand-typed formulas.
- docs/guide/file-import.md: corrected the claim that HyperFormula 'ignores' the
_xlpm. prefix. It only strips a prefix in front of a function call; _xlpm. never
appears there in a real .xlsx — it always prefixes a bare LAMBDA/LET parameter
name, which this fix deliberately does not touch (HF has no LAMBDA/LET, so such a
formula is #NAME? either way).
- LexerConfig.ts: added {type} JSDoc annotations to match the majority convention
already used elsewhere in src/parser/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1771 +/- ##
========================================
Coverage 97.32% 97.32%
========================================
Files 195 195
Lines 15739 15751 +12
Branches 3390 3462 +72
========================================
+ Hits 15318 15330 +12
+ Misses 421 413 -8
- Partials 0 8 +8
🚀 New features to boost your workflow:
|
GreenFlux
approved these changes
Sep 15, 2026
GreenFlux
left a comment
Contributor
There was a problem hiding this comment.
Looks good, thanks for the quick response!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Problem
The parser requires a function name to start with a letter, so it rejects formulas
that carry the internal prefixes Excel writes into
.xlsxfor every function addedafter ~2007 (
_xlfn.,_xlws., and combinations) — e.g._xlfn.IFS(...). Whether auser hits this depends on their import library: ExcelJS (the library our own
file-import guide uses in its example) passes the
prefixes straight through, and SheetJS strips
_xlfn.but not_xlws., soFILTERand
SORTstill fail. Registering the prefixed name as a custom function is not aworkaround — the lexer rejects the leading underscore before name resolution runs.
Fixes #1655
Fix
_xlfn.,_xlfn._xlws.,_xlws.,_xlpm.,_xludf.)are now an optional part of the
ProcedureNameandOffsetProcedureNamelexertokens, so
_xlfn.IFS(...)tokenizes the same asIFS(...).A string-level
replace(the fix sketched in the issue) also rewrites the prefixinside string literals —
="_xlfn.IFS is a literal"would silently become="IFS is a literal". There's a regression test for this.FormulaParserandParserWithCaching.computeHashFromTokensboth derive thecanonical function name through shared helpers (
canonicalProcedureNameFromToken,canonicalOffsetProcedureNameFromToken), so a prefixed formula and its unprefixedequivalent — OFFSET included — produce the same cache hash.
getCellFormula()/getCellSerialized()return the formula without the prefix.Scope notes:
_xlpm.xidentifier (aLAMBDA/LETparameter, not a call) is notstripped — HyperFormula has no
LAMBDA/LET, so such a formula is#NAME?either way.
ProcedureName(every ordinaryfunction), which is all Excel ever writes.
OffsetProcedureNameis the oneexception — see "Post-review changes" below.
_xlfn.CONCAT(...)is#NAME?(CONCAT isn'timplemented), not a parsing error — strictly better than before, not silently
swallowed.
Changed files
src/parser/parser-consts.tsEXCEL_INTERNAL_FUNCTION_PREFIX_PATTERNsrc/parser/LexerConfig.tsProcedureName/OffsetProcedureNameaccept an optional prefix; newcanonicalProcedureNameFromToken()/canonicalOffsetProcedureNameFromToken()helperssrc/parser/FormulaParser.tssrc/parser/ParserWithCaching.tsdocs/guide/file-import.mdCHANGELOG.mdTests
Regression tests in
handsontable/hyperformula-tests(branchfix/hf-217-excel-internal-prefixes, 28 tests, all pass):getCellFormula,getCellSerialized, shared cache hash (incl. OFFSET, two prefixes)#NAME?not a parse error, upper-cased prefix rejected (except OFFSET, pinned separately), unrelated underscore name, prefix alonesrc/at each stage: the relevant new tests go red,confirming they detect the bug rather than passing regardless.
npm run test:performance: exit 0. Targeted lexer microbenchmark (parser is onthe
buildFromSheetshot path) shows no measurable regression across everyround of changes below.
Post-review changes
An exhaustive multi-agent review (5 independent dimensions, each finding
adversarially verified by 3 more agents before being trusted) ran against this PR
after the first green CI, on top of my own manual review. It surfaced 17 raw
findings; 6 survived verification, 3 of which converged independently on the same
root cause. What changed as a result:
links Fix: parser rejects formulas with Excel's internal prefixes (#1655) #1771, matching the majority of neighboring Unreleased entries.
_xlfn.OFFSET(...)andOFFSET(...)did not share a cache hash, unlikeevery other function —
computeHashFromTokensonly canonicalizedProcedureNametokens;OffsetProcedureNameis a separate, uncategorized tokentype (OFFSET has its own grammar rule) and fell through to the generic
raw-image branch. Verified empirically before/after with the parser's own
tokenizeFormula(). Fixed with a dedicated branch +canonicalOffsetProcedureNameFromToken().canonicalProcedureNameFromTokenwas measurably slower per call than theinline code it replaced for the common (no-prefix) case — real but bounded
(the full-pipeline lexer benchmark never showed it). Added a cheap
leading-underscore guard before invoking the prefix-stripping regex at all,
since every prefix starts with
_. A reorder alternative suggested by thereview was measured and found to not help (and to slightly hurt the prefixed
case), so it was not adopted — verifying a benchmark's own claim mattered here.
OffsetProcedureName's prefix matching is case-insensitive, unlikeProcedureName—_XLFN.OFFSET(...)is silently accepted where_XLFN.SUM(...)is a parse error. Real and now documented and pinned by a test, not fixed in
code: OFFSET predates the OOXML cutoff this whole prefix scheme exists for, so
Excel can never actually emit a prefixed OFFSET call in any case — a dedicated
case-sensitive-prefix matcher would add real lexer complexity for an input
nothing can produce.
_xlfn.Sumis shadowed by a call toSUMthe moment it's followed by
(. Verified this is pre-existing, general enginebehavior (an unprefixed named expression named
Sumis shadowed identically,unrelated to this PR) — this PR only changes what used to be a parse error into
the same precedence rule every other name already follows. Pinned with a test
rather than changed.
docs/guide/file-import.mdoverclaimed_xlpm.handling — it saidHyperFormula "ignores" the prefix, but the code only strips a prefix in front of
a function call, and
_xlpm.never appears there in a real.xlsx(it alwaysprefixes a bare
LAMBDA/LETparameter name, which this fix deliberately doesnot touch). Corrected.
Also found and fixed in the same pass: the existing "shares one cache entry" test
tokenized through a separately-built lexer instead of the parser's own
tokenizeFormula()— harmless forProcedureName(a module-level singleton tokentype) but would have silently never caught a bug in
OffsetProcedureName(rebuiltfresh per
buildLexerConfig()call, so a second lexer's token fails the identitycheck
tokenMatcherrelies on). Both the existing test and the new ones nowtokenize via the parser's own method.
🤖 Generated with Claude Code
Note
Medium Risk
Changes core formula lexing/parsing on a hot path for every function call, but behavior is additive (ignore known prefixes) with shared canonicalization for caching; main risk is incorrect prefix matching edge cases.
Overview
Fixes formula parsing when workbooks are imported with Excel’s internal function-name prefixes (
_xlfn.,_xlfn._xlws.,_xlws.,_xlpm.,_xludf.), which commonly appear in.xlsxstrings from parsers such as ExcelJS. Formulas like=_xlfn.IFS(...)previously failed at lex time; they now tokenize and evaluate like=IFS(...).The lexer treats those prefixes as optional on
ProcedureNameandOffsetProcedureName, strips them via sharedcanonicalProcedureNameFromToken/canonicalOffsetProcedureNameFromTokenhelpers (fast path when the name does not start with_), and uses the same canonicalization inParserWithCachingso prefixed and unprefixed formulas share a cache hash. Prefixes are not rewritten in the raw formula string, so text inside string literals stays unchanged.getCellFormula()is documented to return the unprefixed form; unsupported functions still yield#NAME?, not a parse error.Docs in
file-import.mdandCHANGELOG.mddescribe the prefixes and import-library behavior.Reviewed by Cursor Bugbot for commit fbf7306. Bugbot is set up for automated code reviews on this repo. Configure here.