Skip to content

Fix line counting in get_location and print_block_string - #295

Merged
Cito merged 1 commit into
graphql-python:mainfrom
arxhr007:fix/splitlines-location-parity
Nov 5, 2026
Merged

Cito merged 1 commit into
graphql-python:mainfrom
arxhr007:fix/splitlines-location-parity

Conversation

@arxhr007

@arxhr007 arxhr007 commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #294

Summary

Source.get_location() and print_block_string() used str.splitlines(), which recognizes more line boundaries than the GraphQL spec / graphql-js (\r\n, \n, \r only). This produced three issues, all fixed by splitting on \r\n|[\n\r] — the same regex print_source_location() already uses:

  1. IndexError crash: an error location computed via get_location() could reference a line that doesn't exist in print_source_location()'s line array (e.g. source containing a vertical tab), making str(GraphQLSyntaxError) itself raise.
  2. Wrong line numbers at line starts: splitlines() drops the trailing segment after a final newline, so every character immediately following a line terminator was reported on the previous line (Source("a\nb").get_location(2) → (1,2) instead of (2,1)).
  3. Location/serialization drift from graphql-js: \v, \f, \x1c–\x1e, \x85, 
, 
 were counted as line breaks in get_location() (graphql-js counts only \r\n|[\n\r]), and print_block_string() split values on the same too-broad set.

Found by differential-fuzzing parse/print output against graphql-js on mutated documents. After the fix, error locations and printed block strings match graphql-js byte-for-byte on the cases that previously diverged (e.g. "Q\x0b… → both report Unterminated string at 1:30).

Test plan

  • pytest tests/ — 3,369 passed, 17 skipped
  • New regression tests: line-start + non-GraphQL-terminator locations (test_source.py), get_location-derived location rendering (test_print_location.py), non-GraphQL line breaks in print_block_string (test_block_string.py)
  • ruff check + ruff format --check clean on all touched files

Use the same GraphQL line terminator set (LF, CR, CRLF) that graphql-js
and print_source_location() use, instead of str.splitlines() which also
splits on VT, FF, FS/GS/RS, NEL and U+2028/U+2029 and drops a trailing
empty line segment.

This fixes three issues:
- characters right after a line terminator were reported on the
  previous line
- characters after non-GraphQL line breaks got wrong line numbers
- print_source_location() could crash with IndexError when an error
  location pointed past the last printed line (issue #294)

print_block_string() also splits raw values into lines and had the
same drift: values containing e.g. VT printed as multi-line in Python
but single-line in graphql-js.
@arxhr007
arxhr007 requested a review from Cito as a code owner November 3, 2026 11:28
@codspeed

codspeed Bot commented Nov 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing arxhr007:fix/splitlines-location-parity (7524332) with main (f9bdf7c)

Open in CodSpeed

@Cito

Cito commented Nov 5, 2026

Copy link
Copy Markdown
Member

Thanks for reporting and fixing this! It is a clear improvement, so I merge it as it is. I still plan some follow-up changes to bring this code fully in line with GraphQL.js.

Fixed by this PR on main:

  • str(error) no longer raises IndexError when the source contains characters like \v, \f, \x85 or \u2028.
  • get_location now reports the correct line for positions at the start of a line, and only counts \r\n, \n and \r as line terminators.
  • print_block_string now splits lines the same way as GraphQL.js.

Planned as part of the regular porting from GraphQL.js:

Also planned:

  • Backporting the fix to the 3.2.x line, because this PR only targets main.
  • Removing the leftover or [""] in print_block_string, which can no longer take effect.

I'll take care of that, you don't need to send a separate PR.

@Cito
Cito merged commit 8e04855 into graphql-python:main Nov 5, 2026
11 checks passed
@Cito

Cito commented Nov 5, 2026

Copy link
Copy Markdown
Member

The follow-up is done, on main and on js-16.x.x, and will ship in 3.3.2 and 3.2.14:

  • get_location now uses the same scanning algorithm as GraphQL.js (60bcd297). Positions inside a \r\n report column 0 and positions outside the source body also match now. I compared it with GraphQL.js v17.1.0 and v16.15.0 on about 100,000 positions and found no differences.
  • The location tests from test: cover source location lookups graphql/graphql-js#4935 are ported.
  • The leftover or [""] in print_block_string is gone (0aaea717).
  • This PR and the changes above are backported to the 3.2.x line (001ca2c2, which keeps your authorship, and 2e681e64).

The upstream performance changes that share location lookups (graphql/graphql-js#4936 and graphql/graphql-js#4958) follow with the regular porting.

Thanks again for reporting and fixing this!

@Cito

Cito commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

The follow-up is done, on main and on js-16.x.x, and shipped in 3.3.2 and 3.2.14:

  • get_location now uses the same scanning algorithm as GraphQL.js (60bcd297). Positions inside a \r\n report column 0 and positions outside the source body also match now. I compared it with GraphQL.js v17.1.0 and v16.15.0 on about 100,000 positions and found no differences.
  • The location tests from test: cover source location lookups graphql/graphql-js#4935 are ported.
  • The leftover or [""] in print_block_string is gone (0aaea717).
  • This PR and the changes above are backported to the 3.2.x line (001ca2c2, which keeps your authorship, and 2e681e64).

The upstream performance changes that share location lookups (graphql/graphql-js#4936 and graphql/graphql-js#4958) are ported as well (0a57746d, 1c7791b8, and on 3.2.x 3489973c, a0e66c11).

Thanks again for reporting and fixing this!

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.

get_location() uses str.splitlines(): wrong line numbers, drift from graphql-js, and IndexError crash in error printing

2 participants