Skip to content

fix(diagnostics): map solc offsets against the live buffer, not disk - #231

Draft
rifuki wants to merge 1 commit into
asyncswap:mainfrom
rifuki:fix/diagnostics-live-buffer-offsets
Draft

rifuki wants to merge 1 commit into
asyncswap:mainfrom
rifuki:fix/diagnostics-live-buffer-offsets

Conversation

@rifuki

@rifuki rifuki commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #235.

Both call sites in on_change now pass &params.text — the same text handed to solc — instead of re-reading the file from disk to map offsets.

I traced the three callers (did_open, run_did_save, did_create_files) and in each one the String passed as params.text is the one solc_ast() inlines as the source unit's "content", so the two can't disagree. Imports are left on disk deliberately: they reach solc as "urls", so solc reads them itself and cross_file_error_diagnostics is already right.

Two regression tests, one per failure mode. With the buffer longer than disk the offset runs past EOF and clamps — main reports (10,0)-(10,0) where the error is at (12,16)-(12,30). With the buffer shorter it lands inside the disk text and gives a plausible wrong line — main reports (6,5)-(7,0) instead of (7,16)-(7,30). Both assert the full range and both fail on main.

They wait for two publishDiagnostics before dirtying the buffer, since on_change sends an empty one up front to clear stale squiggles before solc runs — waiting for one races the build. Lint is left on so the branch exercised is the one a real Foundry project takes. Like tests/build.rs they need forge on PATH, which CI installs.

cargo test 666 passed, clippy and fmt clean.

The test harness is copied from tests/lsp_file_ops_settings.rs. I can pull it out into a shared tests/common/mod.rs if you'd rather, but that touches another test file so I left it out of this one.

`on_change` passes the editor's live buffer to solc — the comment above the
call says so explicitly — but then converted the byte offsets solc returned
into LSP positions using `tokio::fs::read_to_string(&file_path)`, a separate
read of the file on disk. solc reports offsets into the text it was given, so
resolving them against different text puts every diagnostic in the file at the
wrong position.

The obvious objection is that at didSave the editor has just flushed the
buffer, so disk should match. Three things break that:

- A client may omit the optional `text` on didSave. `run_did_save`
  (src/lsp.rs:1721) then falls back to the text_cache — the live buffer,
  unsaved edits included — while disk still holds the last-saved content.
- Saves are collapsed. `did_save` (src/lsp.rs:3540) routes each URI through a
  watch channel and the worker picks up the *latest* params via
  `borrow_and_update` (src/lsp.rs:3581), so a save can be processed after
  further edits have landed.
- The scaffold-recovery path compiles generated content that was delivered by
  applyEdit and never written, so disk is empty and `byte_offset_to_position`
  collapses every diagnostic onto line 0.

Both call sites now pass `&params.text`, the same text handed to solc. Imports
are unaffected and deliberately still resolved against disk: they reach solc
as `"urls"` rather than inlined content, so solc reads them from disk itself.

Adds two protocol-level regression tests, covering both ways the mapping goes
wrong. With the buffer longer than disk the bad offset runs past end-of-file
and clamps — main reports (10,0)-(10,0) instead of (12,16)-(12,30). With the
buffer shorter, the offset still lands inside the on-disk text and yields a
wrong-but-plausible line — main reports (6,5)-(7,0) instead of (7,16)-(7,30),
which is the variant that survives a bug report. Both assert the full range,
and both fail on main and pass here.

Like tests/build.rs the tests need the Foundry toolchain on PATH, which CI
already installs. They wait for two publishDiagnostics payloads before
dirtying the buffer, because `on_change` emits an empty one up front to clear
stale squiggles before solc runs; waiting for one would race the build. Lint
is left enabled so the exercised branch is the one a real Foundry project
takes.

The test harness is duplicated from tests/lsp_file_ops_settings.rs. Happy to
extract a shared tests/common/mod.rs in a follow-up if preferred — it is left
out here to keep this diff to the fix.
@rifuki
rifuki marked this pull request as draft August 25, 2026 10:01
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.

Diagnostics land on the wrong line when the buffer differs from the file on disk

1 participant