Skip to content

fix/json unicode offsets - #2659

Open
jurgenvinju wants to merge 28 commits into
mainfrom
fix/json-unicode-offsets
Open

jurgenvinju wants to merge 28 commits into
mainfrom
fix/json-unicode-offsets

Conversation

@jurgenvinju

@jurgenvinju jurgenvinju commented Feb 18, 2026 •

Copy link
Copy Markdown
Member

Fixes unicode offsets for the JSON parser/validator:

  • parse error locations
  • origin src keyword fields
  • fully satisfies the loc semantics of vallang and Rascal (offset, length, line, column)

This makes JSON parsers ready for use in an editor/UI context. From that perspective, this was a bug. From the "we had a reasonable JSON parser" perspective, this was an enhancement.

Instruments the OriginTrackingReader embedded in JSONValueReader to accurately deal with the presence of unicode surrogate pairs in the char buffer of the reader.

Note that unicode characters in comments are equally responsible for shifts in the offsets as unicode characters in string constants and field names.

  • start of buffer offset compensated for presence of surrogate pairs
  • pos index into buffer compensated for surrogate pairs
  • column position (same)
  • line position (same)
  • asserts added to reflect the many many assumptions the links between GsonReader, OriginTrackingReader and JSONValueReader depend on.
  • comments to explain the above as well; it's rather finicky
  • added specific file with unicode for testing purposes
  • added JSON generators with different shapes of unicode to the JSONIOTests, specifically for triggering exceptional conditions around the buffer limits:
    • high surrogate and low surrogate before the limit
    • high surrogate before the limit and low surrogate after the limit
    • several surrogate pairs in a row around the limit
    • alternated surrogate pairs and normal chars around the limit

The current solution still streams quickly and scales freely to very long JSON content, very long lines in JSON content, and very long comments or strings in JSON content.

@codecov

codecov Bot commented Feb 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.24590% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 45%. Comparing base (9115006) to head (b5aa39d).

Files with missing lines Patch % Lines
...pl/library/lang/json/internal/JsonValueReader.java 85% 2 Missing and 7 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##              main   #2659   +/-   ##
=======================================
  Coverage       45%     45%           
- Complexity    6807    6812    +5     
=======================================
  Files          844     844           
  Lines        68823   68865   +42     
  Branches     10038   10053   +15     
=======================================
+ Hits         31415   31457   +42     
  Misses       35005   35005           
  Partials      2403    2403           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jurgenvinju jurgenvinju self-assigned this Feb 19, 2026
@jurgenvinju
jurgenvinju marked this pull request as ready for review February 24, 2026 13:54
@DavyLandman

Copy link
Copy Markdown
Member
Error: RROR] /home/runner/actions-runner/_work/rascal/rascal/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java:[1032,17] cannot find symbol
  symbol: variable lineHandler
Error: RROR] /home/runner/actions-runner/_work/rascal/rascal/src/org/rascalmpl/library/lang/json/internal/JsonValueReader.java:[1033,17] cannot find symbol

I think there's some uncommitted file?

@jurgenvinju

Copy link
Copy Markdown
Member Author

Yes, there was an uncommitted file. Thanks

@sonarqubecloud

sonarqubecloud Bot commented Apr 1, 2026

Copy link
Copy Markdown

@DavyLandman DavyLandman 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.

I think this looks good, but I have the feeling it's duplicating code we already have in the ColumnOffset code. I suspect we'll be able to reduce the size of this PR (and thus new code to maintain) if we can reuse that.

Comment on lines +1231 to +1232
// For every high surrogate we assume a low surrogate will follow,
// and we count only one of them for the character offset by increasing `shift`

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.

this would mean that a broken pair causes offsets? is that fine?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

probably fine, but it is possible to make this code more robust against some common cases.

* See the body of {@link JsonReader#fillBuffer(int minimum)} for the contract that we must satisfy and
* the preconditions we are given at every call to {@link #read(char[], int, int)}.
*/
public static class OriginTrackingReader extends FilterReader {

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.

a lot of this code reminds me of the ColumnMaps and LineColumnOffsetMap (and its implementation ArrayLineOffsetMap). They've been migrated to rascal a while back, so maybe we can reuse that code here?

Perhaps the LineColumnOffsetMap needs to learn a few new features, but it has been used for quite a few years in rascal-lsp for a very similar job.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The current solution is tightly integrated into the streaming/buffering semantics of the parser that we are re-using. We use internal/private counters and offsets from this implementation, and depend highly on the state and changes of state (when the buffer is being re-filled).

The point of all this is to keep a streaming semantics and not fill a buffer the size of the file, and try to be as fast as Gson can be. I don't understand yet what that would mean for using the LineColumnOffsetMap, and the mapping of lines and columns is not the challenge that this code solves.

The main challenge that is solved here is to find the start and end character offsets (UTF32) of every JSON node and field while the reused parser/reader is unwilling to give up this information. So we recover the information from its internal buffer on-the-go and synchronizing with buffer resets.

@DavyLandman
DavyLandman force-pushed the fix/json-unicode-offsets branch 2 times, most recently from 4aaad45 to 5dfa802 Compare June 26, 2026 12:02
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants