ClosedXML / ClosedXML/ClosedXML.Parser

IndexOutOfRangeException parsing a structured reference whose specifier list has no column (Table1[[#All]], Table1[[#Headers],[#Data]])

Open Beginner friendly
#31 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
14
Forks
8
PR merge metrics
No merged PRs in 30d

Description

A structured reference whose bracketed inner form ends with an item specifier and no column name - `Table1[[#All]]`, `Table1[[#Headers],[#Data]]` - throws `IndexOutOfRangeException` out of the tokenizer instead of parsing. Both are valid Excel.

It matters because the exception is reachable from file input: any workbook holding such a formula or defined name fails to load with a runtime exception rather than the library's own `ParsingException`, so a consumer cannot distinguish "malformed formula" from a bug and cannot catch it as a parse failure.

## Reproduction

Against `ClosedXML.Parser` 2.0.0 from NuGet, calling `CellFormulaA1` with a no-op factory so only the tokenizer is exercised:

| Formula | Result |
|---|---|
| `Sales[#All]` | ok |
| `Sales[#Headers]` | ok |
| `Sales[[#Headers],[Amount]]` | ok |
| `Sales[[#Headers],[#Data],[Amount]]` | ok |
| `Sales[[#Headers],[#Data],[Amount]:[Tax]]` | ok |
| `Sales[[#All]]` | **IndexOutOfRangeException** |
| `Sales[[#Headers]]` | **IndexOutOfRangeException** |
| `Sales[[#Headers],[#Data]]` | **IndexOutOfRangeException** |
| `Sales[[#Data],[#Totals]]` | **IndexOutOfRangeException** |
| `Sales[ [#Headers],[#Data] ]` | **IndexOutOfRangeException** |
| `Sales[[#All],[#Data]]` | ParsingException *(correct - illegal in Excel)* |
| `Sales[[#Headers],[#Totals]]` | ParsingException *(correct - not contiguous)* |

The last two rows are the right behaviour: Excel only permits the contiguous pairs `#Headers,#Data` and `#Data,#Totals`, and the tokenizer rejects the others properly. So the accepted grammar is correct; it just crashes on the legal inputs that end without a column.

The rule is not "two specifiers" - `Sales[[#Headers]]`, with one, fails too. It is: **in the `[[…]]` inner-reference form, a specifier list not followed by a column name crashes.** The unbracketed shorthand `Sales[#All]` is fine because it returns early on a different branch.

```
System.IndexOutOfRangeException: Index was outside the bounds of the array.
at ClosedXML.Parser.TokenParser.GetStructuredName(ReadOnlySpan`1 input, Int32 startIdx, String& columnName)
at ClosedXML.Parser.TokenParser.ParseIntraTableReference(ReadOnlySpan`1 input, StructuredReferenceArea& area, String& firstColumn, String& lastColumn)
at ClosedXML.Parser.FormulaParser`3.RefAtomExpression(Boolean replaceFirstAtom, TNode refAtom)

at ClosedXML.Parser.FormulaParser`3.CellFormulaA1(String formula, TContext context, IAstFactory`3 factory)
```

## Cause

`src/ClosedXML.Parser/TokenParser.cs`, `ParseIntraTableReference` (lines ~386–419 on `develop`). Each keyword block ends with an unconditional `SkipComma`, which assumes a column always follows the specifier list:

```csharp
if (input[i + 1] == '#')
{
var listItem = GetArea(input, ++i);
i += GetLength(listItem) + 1;
area |= listItem;

i = SkipComma(input, i); // <-- assumes a comma is next
}

// … second keyword block, same shape …

// After keyword list, we get either a COLUMN or a COLUMN:COLUMN
i = GetStructuredName(input, i, out firstColumn);
```

Tracing `Sales[[#Headers]]`, where the intra-table span is `[[#Headers]]` (length 12):

1. `i = 1`, `input[1] == '['`, so control reaches the inner-reference path.
2. First keyword block: `GetArea` returns `Headers`, then `i += GetLength(Headers) + 1` → `i = 2 + 8 + 1 = 11`, and `input[11] == ']'` - the reference is over.
3. `SkipComma(input, 11)` is called anyway. Its `Debug.Assert(input[i] == ',')` is exactly the violated invariant, but it compiles out in Release, so it does `i++` and returns `12 == input.Length`.
4. `GetStructuredName(input, 12, …)` reads `input[startIdx]` and throws.

`Sales[[#Headers],[#Data]]` reaches the same place through the second block: after `#Data`, `i` lands on the closing `]`, `SkipComma` walks past the end, and `GetStructuredName` indexes out of range.

That also explains why the three-part forms are fine - there really is a comma and a column after the list, so the assumption holds.

## Suggested fix

Only skip a comma when one is actually there, and treat a specifier list that ends at `]` as a complete reference with no column:

- after each keyword block, look at the next non-whitespace character;
- if it is `]`, set `firstColumn = lastColumn = null` and return;
- if it is `,`, consume it and carry on as now.

That keeps every currently-passing input on the same path and turns the four crashes into the areas they denote (`#All` → whole table, `#Headers`+`#Data` → header and data rows over the table's full width). It also leaves the `ParsingException` rejections untouched.

## Notes

- Reproduced against the 2.0.0 NuGet package; the `develop` source shows the same unconditional `SkipComma`, so it looks unfixed there too.
- I have not checked the R1C1 entry point, only `CellFormulaA1`.
- Downstream, `StructuredReferenceArea` is a `[Flags]` enum and consumers already handle `Headers | Data` and `Totals | Data`, so nothing on the AST side needs to change - only the tokenizer.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in src/ClosedXML.Parser/TokenParser.cs at ParseIntraTableReference and reproduce the cases through CellFormulaA1 with the no-op factory. Verify that legal structured references ending in a specifier list parse without IndexOutOfRangeException, while the listed invalid combinations still produce ParsingException.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
compilers
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
84/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.