Skip to content

Fix the position of the end of a file - #1352

Merged
clauderic merged 1 commit into
cyclic-extendsfrom
offense-end-at-eof
Oct 20, 2026
Merged

clauderic merged 1 commit into
cyclic-extendsfrom
offense-end-at-eof

Conversation

@clauderic

@clauderic clauderic commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1351. Review that one first; this PR's base moves down the stack as the PRs below it merge.

What are you adding in this PR?

An offense that ends at the end of a file ended one character short, or on the line before when the file ends with a newline.

  • getPosition used line-column's fromIndex, which only has positions for characters, not for the end of the file. So it clamped the index to the last character.

  • The index was right, but the CLI and the editor use line and character. The language server's diagnostics come from them, so editor underlines were short too.

  • getPosition now finds the line by scanning for \n up to the index, as line-column splits on \n only. A negative index, like the end of an unclosed block, still has line and character -1. getOffset is unchanged.

  • It's also cheaper. line-column split the whole file into lines on every call, and report calls it twice per offense. Scanning allocates nothing and stops at the index:

    Offenses old new
    50 KB file, 20 offenses 1.0 ms 0.2 ms
    1 MB file, 1,000 offenses 652 ms 193 ms
    20 MB file, 1 offense 12.2 ms 3.7 ms

All existing tests pass unchanged; none covered the end of a file.

What's next? Any followup issues?

#1347 is stacked on this. Its specs record the corrected positions.

Tophatting

  • npx vitest run packages/theme-check-common/src/utils/position.spec.ts
  • Or: {{ content_for_header | split: ',' }}, alone in a file, reports ContentForHeaderModification over the whole file. Before, the offense ended at [0, 36], on the last }; now it ends at [0, 37].

Before you deploy

  • I included a patch bump changeset
  • My feature is backward compatible

🤖 Generated with Claude Code

@clauderic
clauderic requested a review from a team as a code owner October 20, 2026 13:15
@clauderic clauderic mentioned this pull request Oct 20, 2026
2 tasks done
@clauderic
clauderic added this pull request to stack #1353 October 20, 2026 13:15
const lineCol = lineColumn(source, { origin: 0 }).fromIndex(Math.min(index, source.length - 1));

// Not lineColumn's fromIndex, which has no position for the end of the file.
const before = source.slice(0, index);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I feel like that's not a performant way of doing that.

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.

Good call. It's a scan with indexOf now, with no allocation, and it stops at the index.

For what it's worth, the old lineColumn(source) was slower still: it split the whole file into lines on every call, and report calls it twice per offense.

Case Before this PR (line-column) First version (slice + split) Now (indexOf scan)
50 KB file, 20 offenses 1.0 ms 0.3 ms 0.2 ms
1 MB file, 1,000 offenses 652 ms 290 ms 193 ms
20 MB file, 1 offense 12.2 ms 5.7 ms 3.7 ms

How it was measured: Node 24.15 on an M3 Pro. Each file repeats one ~55-character line of Liquid, and the offenses are spread evenly through it. Each offense makes two calls, start and end, as report does. Times are one run, after a warm-up. The scan gives the same positions as the slice + split version, checked on edge cases and across the 50 KB file.

A line table built once per file would turn each lookup into a binary search, but it needs a cache. It seemed worth adding only if large files with thousands of offenses turn out to matter.

@charlespwd charlespwd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

perf impact of this one is probably not good. source.slice for getPosition seems wrong.

@clauderic
clauderic force-pushed the offense-end-at-eof branch 2 times, most recently from bf22331 to 7f38a55 Compare October 20, 2026 15:28
@clauderic
clauderic requested a review from charlespwd October 20, 2026 16:16
getPosition used line-column's fromIndex, which only has positions for
characters, so it clamped the index to the last character. An offense
ending at the end of a file ended one character short, or on the line
before when the file ends with a newline. The index was right, but the
CLI and the editor use the line and character.

It now finds the line by scanning for "\n" up to the index, as
line-column splits on "\n" only. That's also cheaper: line-column
split the whole source into lines on every call, twice per offense. A
negative index, like the end of an unclosed block, still has line and
character -1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@clauderic
clauderic merged commit 20f8edf into main Oct 20, 2026
8 checks passed
@clauderic
clauderic deleted the offense-end-at-eof branch October 20, 2026 16:28
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.

2 participants