Skip to content

fix: parse a keyword list that is a whole inner reference - #32

Open
jafin wants to merge 1 commit into
ClosedXML:developfrom
jafin:fix/keyword-list-without-column
Open

fix: parse a keyword list that is a whole inner reference#32
jafin wants to merge 1 commit into
ClosedXML:developfrom
jafin:fix/keyword-list-without-column

Conversation

@jafin

@jafin jafin commented Aug 1, 2026

Copy link
Copy Markdown

Fixes #31

The grammar has two alternatives; only one was implemented

FormulaLexer.g4:

fragment INNER_REFERENCE
        : KEYWORD_LIST
        | (KEYWORD_LIST SPACED_COMMA)? COLUMN_RANGE
        ;

TokenParser.ParseIntraTableReference only handled the second. Each keyword block ended with an unconditional SkipComma, which assumes a column range follows the keyword list — so the first alternative walked off the end of the span.

Tracing [[#All]] (length 8):

  1. GetArea reads #All, then i += GetLength(All) + 1i = 7, and input[7] == ']'. The reference is over.
  2. SkipComma(input, 7) is called anyway. Its Debug.Assert(input[i] == ',') is precisely the violated invariant, but it compiles out in Release, so it does i++ and returns 8 == input.Length.
  3. GetStructuredName(input, 8, …) reads input[startIdx] and throws IndexOutOfRangeException.

[[#Headers],[#Data]] reaches the same place via the second keyword block.

Every bare-keyword-list form the grammar allows was affected:

Input Before After
[[#All]] IndexOutOfRangeException All, no columns
[[#Headers]] IndexOutOfRangeException Headers, no columns
[[#Data]] / [[#Totals]] / [[#This Row]] IndexOutOfRangeException that area, no columns
[[#Headers],[#Data]] IndexOutOfRangeException Headers | Data, no columns
[[#Data],[#Totals]] IndexOutOfRangeException Data | Totals, no columns
[ [#All] ] IndexOutOfRangeException All, no columns

The lexer accepts all of them — AssertTokenType(…, INTRA_TABLE_REFERENCE) passes for each — so this was reachable from any workbook holding one, and surfaced as a runtime exception rather than as ParsingException.

The shorthand [#All] was never affected: it returns early on the KEYWORD branch, before this code.

The fix

Each keyword block now asks whether the inner reference is over — only the SPACED_RBRACKET left — and returns the keyword list with no columns when it is:

private static bool IsEndOfInnerReference(ReadOnlySpan<char> input, int i)
{
    i = SkipWhitespaces(input, i);
    return i >= input.Length || input[i] == ']';
}

A comma is skipped only when one is actually there, so every input that parsed before takes exactly the same path as before. The two ParsingException rejections for the illegal combinations — [[#All],[#Data]] and [[#Headers],[#Totals]], neither of which is a legal KEYWORD_LIST — are untouched and still rejected.

Tests

Two existing tests were already parked on this bug. FormulaConverterToR1C1Tests.StructureReference carried

[InlineData("[[#Headers],[#Data]]", 10, 15, "[[#Headers],[#Data]]", Skip = "Parser fail")]
[InlineData("[[#Data],[#Totals]]", 10, 15, "[[#Data],[#Totals]]", Skip = "Parser fail")]

Both are unskipped here and pass, round-tripping unchanged. Added [[#All]] and [[#Headers]] alongside them, which normalise to the [#All] / [#Headers] shorthand on the way out.

Also added, following each file's existing grammar-comment style:

  • IntraTableReferenceTokenTests — all five single keywords, both two-item lists, and the whitespace-padded forms. This test asserts the token type first, so it also pins that the lexer accepts them.
  • StructureReferenceRuleTests — the node-level shape, with and without a table name.

INNER_REFERENCE has two alternatives:

    fragment INNER_REFERENCE
            : KEYWORD_LIST
            | (KEYWORD_LIST SPACED_COMMA)? COLUMN_RANGE
            ;

ParseIntraTableReference only implemented the second. Each keyword block ended
with an unconditional SkipComma, which assumes a column range follows the
keyword list, so the first alternative walked off the end of the span:
SkipComma stepped past the closing bracket - its Debug.Assert on the comma is
exactly the violated invariant, and compiles out in Release - and
GetStructuredName then indexed input[input.Length].

Every form the grammar allows with a bare keyword list therefore threw
IndexOutOfRangeException rather than parsing: [[#All]], [[#Headers]] and the
two-item lists [[#Headers],[#Data]] and [[#Data],[#Totals]]. The lexer accepts
them all, so this was reachable from any workbook holding one, and it escaped
as a runtime exception rather than as ParsingException.

Each keyword block now checks whether the inner reference is over - only the
SPACED_RBRACKET left - and returns the keyword list with no columns when it is.
A comma is skipped only when one is actually there, so every input that parsed
before takes the same path.

The two InlineData cases marked Skip = "Parser fail" in
FormulaConverterToR1C1Tests are this bug; they are unskipped and pass.
Copilot AI review requested due to automatic review settings August 1, 2026 01:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a tokenizer crash when parsing structured references whose [[...]] inner-reference consists solely of a keyword list (e.g. [[#All]], [[#Headers],[#Data]]) by correctly treating the keyword list as a complete inner reference with no column range.

Changes:

  • Update TokenParser.ParseIntraTableReference to detect end-of-inner-reference after parsing keyword list items and return with firstColumn/lastColumn = null when appropriate.
  • Add/extend lexer and rule-level tests to cover single-keyword and two-keyword-list inner references (including whitespace-padded forms).
  • Unskip and extend existing R1C1 conversion round-trip tests for previously failing structured references.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/ClosedXML.Parser/TokenParser.cs Prevents walking past the end of the intra-table-reference span when the inner reference ends after a keyword list.
src/ClosedXML.Parser.Tests/Rules/StructureReferenceRuleTests.cs Adds AST-shape coverage for keyword-list-only inner references (with/without table name).
src/ClosedXML.Parser.Tests/Lexers/IntraTableReferenceTokenTests.cs Adds tokenization coverage for keyword-list-only inner references and whitespace-padded variants.
src/ClosedXML.Parser.Tests/FormulaConverterToR1C1Tests.cs Unskips previously failing cases and adds round-trip coverage for [[#All]] / [[#Headers]] normalization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants