Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix IndexError on source mixing lone CR with LF line endings (#105) - #174

Merged
dsagal merged 3 commits into
gristlabs:masterfrom
apoorvdarshan:fix-issue-105-crlf-line-endings
Jul 12, 2026
Merged

Fix IndexError on source mixing lone CR with LF line endings (#105)#174
dsagal merged 3 commits into
gristlabs:masterfrom
apoorvdarshan:fix-issue-105-crlf-line-endings

Conversation

@apoorvdarshan

@apoorvdarshan apoorvdarshan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #105.

asttokens.ASTTokens(src, parse=True) raised IndexError: list index out of range on valid source that mixes a lone \r with \n and ends with an attribute access. The minimal reproducer is "\ry.y\n".

Root cause

Two position systems disagreed:

  • ast.parse() treats \r, \r\n, and \n as source line boundaries and reports y.y on line 2.
  • LineNumbers previously recognized only boundaries matched by multiline ^.
  • On Python 3.8–3.11 and PyPy, tokenize emits the entire "\ry.y\n" input as a non-coding NL token, so no y, ., or y tokens exist for the AST nodes.

The AST positions therefore landed on the ENDMARKER; MarkTokens.handle_attr() called next_token() on it and walked past the token list.

Fix

  • Compute source line starts from \r\n, lone \r, and \n sequences.
  • Before tokenization, replace only lone \r with \n. This is length-preserving, so offsets still map to the original source.
  • Preserve \r\n unchanged to avoid shifting later offsets.
  • Apply normalization in the shared asttokens.util.generate_tokens() helper, covering default tokenization and callers that pass generated tokens explicitly.

Tests

  • Added line-number coverage for lone CR, CRLF, and LF boundaries.
  • Added AST and Astroid marking coverage for mixed line endings.
  • Added direct token-generation coverage confirming lone CR normalization and CRLF preservation.
  • Python 3.8: 121 fast tests passed, 1 skipped; 2 slow tests passed; mypy clean.
  • Python 3.11: 123 fast tests passed, 1 skipped; 2 slow tests passed; mypy clean.
  • Python 3.13: 125 fast tests passed, 1 skipped; 2 slow tests passed; mypy clean.
  • The regression passes with astroid<3 on Python 3.8 and 3.11.

Disclosure: prepared with AI assistance; reviewed and verified locally.

Fixes gristlabs#105.

`ASTTokens(src, parse=True)` raised `IndexError: list index out of
range` on valid source that mixes a lone "\r" (old-Mac line ending) with
"\n" and ends with an attribute access, e.g. the minimal reproducer
"\ry.y\n".

`LineNumbers` computed line starts with a `re.M` `^`, which does not
treat a lone "\r" as a line boundary. Python's tokenizer and `ast`
module do, so AST node positions on the line after a "\r" were mapped to
a character offset past the source text and thus onto the ENDMARKER
token. `MarkTokens.handle_attr` then called `next_token` on the
ENDMARKER, walking past the end of the token list.

Compute line starts from the actual end-of-line sequences ("\r\n", "\r"
or "\n") instead, matching how source lines are numbered elsewhere.
"\r\n"- and "\n"-only sources are unaffected.

Add regression tests for the line-offset handling and for marking
tokens on CR/LF-mixed sources.

@dsagal dsagal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!! The fix looks great. I have some comments about strengthening tests.

Comment thread tests/test_line_numbers.py Outdated
def test_carriage_returns(self):
# A lone "\r" (old-Mac line ending) and "\r\n" (Windows) must both count as line boundaries,
# matching how Python's tokenizer and ast module number source lines. See issue #105.
ln = asttokens.LineNumbers("a\rb\r\nc\nd")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suggest a stronger test example like "a\rb\r\nc\n\rd\r\re\r\r\nf" to cover situations like two \r in a row, and \n\r combination

Comment thread tests/test_mark_tokens.py Outdated
# related cases parse and mark without error.
for source in (
"\ry.y\n", # the maintainer's minimal reproducer
"\ry\n",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All these only exercise one lone \r, always before \n. I'd ask for a test string that exercises more combinations, like "a=1\rb.c\r\nd=2\n\re.f\r\rg=3\rh.i". (Then many of the simpler cases are probably not necessary.)

Comment thread tests/test_util.py


def test_generate_tokens_normalizes_lone_carriage_returns():
tokens = list(asttokens.util.generate_tokens("\ry.y\n"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be useful to test triple-quoted strings too?

Comment thread tests/test_mark_tokens.py Outdated
for node in util.walk(atok.tree):
first, last = getattr(node, 'first_token', None), getattr(node, 'last_token', None)
if first is not None:
self.assertNotEqual(last.type, token.ENDMARKER, node)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The assertions check that marking doesn't run off the end of the token stream, but not that nodes get mapped to the right tokens. Maybe you could assert the actual value of atok.get_text(stmt) for some statements?

@apoorvdarshan

Copy link
Copy Markdown
Contributor Author

Thanks for the review! I addressed all four test suggestions in commit 4a4c6f7:

  • expanded the LineNumbers case to cover consecutive \r and \n\r combinations
  • consolidated the token-marking test using the suggested mixed-ending source
  • asserted the exact atok.get_text(stmt) values
  • added coverage for a lone carriage return inside a triple-quoted string

The affected tests pass on Python 3.8, 3.11, and 3.13, and the full fast suite passes on Python 3.13 (125 passed, 1 skipped, 2 deselected).

@dsagal dsagal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you!

@dsagal
dsagal merged commit bb3c487 into gristlabs:master Jul 12, 2026
38 of 39 checks passed
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.

IndexError in asttokens.ASTTokens

2 participants