Skip to content

fix(lsp): prevent corrupted autofix output from overlapping text edits#19793

Merged
Sysix merged 10 commits intooxc-project:mainfrom
wagenet:fix-autofix
Apr 1, 2026
Merged

fix(lsp): prevent corrupted autofix output from overlapping text edits#19793
Sysix merged 10 commits intooxc-project:mainfrom
wagenet:fix-autofix

Conversation

@wagenet
Copy link
Copy Markdown
Contributor

@wagenet wagenet commented Feb 26, 2026

Summary

Fixes garbled file content when applying autofixes via the oxlint language server in editors (e.g. VSCode). Symptoms:

  • import statements or other lines get partially duplicated or have text fragments appended.

  • auto fix on save did not apply because the same range is detected and skipped by the editor

  • Overlapping text edits: fix_all_text_edit emitted TextEdits in non-deterministic HashMap iteration order and did not check for range overlap. The LSP spec requires all TextEdits in a WorkspaceEdit to have non-overlapping ranges; when two diagnostics had fixes touching adjacent spans, applying the conflicting edits produced corrupted output or skipped the fix completly. Fixed by sorting edits by start position and dropping any whose range overlaps the previous one.

closes #20379

…t edits

Two issues caused garbled file content when applying autofixes in editors:

1. `fix_all_text_edit` emitted text edits in non-deterministic HashMap
   iteration order and made no attempt to detect overlap. The LSP spec
   requires that all TextEdits in a WorkspaceEdit have non-overlapping
   ranges; when two diagnostics had fixes touching adjacent spans, VSCode
   would apply the conflicting edits and produce corrupted output. Fix:
   sort edits by start position and drop any edit whose range overlaps
   the previous one.

2. The per-URI code-action cache was never invalidated on `didChange`,
   so code actions computed against an earlier document version could be
   served to the editor after the document had been modified (e.g. after
   a previous fix was applied and then undone). Adding a cache clear in
   `did_change` ensures stale actions are never applied to a document
   they weren't computed for.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wagenet wagenet requested a review from camc314 as a code owner February 26, 2026 20:33
Copilot AI review requested due to automatic review settings February 26, 2026 20:33
@wagenet wagenet requested a review from Sysix as a code owner February 26, 2026 20:33
@github-actions github-actions bot added A-linter Area - Linter A-cli Area - CLI A-editor Area - Editor and Language Server C-bug Category - Bug labels Feb 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes garbled file content when applying autofixes via the oxlint language server by addressing two root causes: overlapping text edits and stale code-action cache.

Changes:

  • Added cache invalidation in didChange handler to prevent serving stale code actions computed against previous document versions
  • Implemented sorting and overlap detection for text edits to ensure LSP spec compliance requiring non-overlapping ranges
  • Updated snapshot test to reflect deterministic (sorted) edit order

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
crates/oxc_language_server/src/backend.rs Added remove_uri_cache call in did_change to invalidate cached code actions when document changes
apps/oxlint/src/lsp/code_actions.rs Added sorting by start position and overlap detection logic to ensure text edits are non-overlapping per LSP spec
apps/oxlint/src/lsp/snapshots/fixtures_lsp_unused_disabled_directives@test.js.snap Updated snapshot to reflect new deterministic edit ordering (line 0 before line 2)

@camc314 camc314 self-assigned this Feb 27, 2026
Copilot AI and others added 5 commits February 27, 2026 19:24
Co-authored-by: wagenet <9835+wagenet@users.noreply.github.com>
Co-authored-by: wagenet <9835+wagenet@users.noreply.github.com>
Co-authored-by: wagenet <9835+wagenet@users.noreply.github.com>
fix(lsp): consolidate range_overlaps into shared utils
@Sysix
Copy link
Copy Markdown
Member

Sysix commented Apr 1, 2026

I updated the code to only change the "fix all" code action / command. I tested the code and it fixes #20379, merging without a second reviewer
Thank you!

@Sysix Sysix added the 0-merge Merge with Graphite Merge Queue label Apr 1, 2026
@Sysix Sysix self-assigned this Apr 1, 2026
@graphite-app
Copy link
Copy Markdown
Contributor

graphite-app bot commented Apr 1, 2026

Merge activity

  • Apr 1, 7:40 PM UTC: @wagenet we removed the merge queue label because we could not find a Graphite account associated with your GitHub profile.

You must have a Graphite account in order to use the merge queue. Create an account and try again using this link

@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Apr 1, 2026
@Sysix Sysix changed the title fix(lsp): prevent corrupted autofix output from overlapping/stale text edits fix(lsp): prevent corrupted autofix output from overlapping text edits Apr 1, 2026
@Sysix Sysix merged commit 5c32fd1 into oxc-project:main Apr 1, 2026
25 checks passed
graphite-app bot pushed a commit that referenced this pull request Apr 5, 2026
This does not fix the stale diagnostics, but we now have at least a clean state on the linter side, when the document changed, but code actions are requested without a diagnostic request before.

Was originally found by #19793 and moved into an own PR for here.
Thank you, @wagenet
leaysgur pushed a commit that referenced this pull request Apr 7, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 22ce6af oxlint/lsp: [**BREAKING**] Show/fix safe suggestions by
default (#19816) (Sysix)

### 🚀 Features

- 7a7b7b8 oxlint/lsp: Add source.fixAllDangerous.oxc code action kind
(#20526) (bab)
- 9cfe57e linter/unicorn: Implement prefer-import-meta-properties rule
(#20662) (Irfan - ئىرفان)
- 1edb391 linter/eslint: Implement `no-restricted-exports` rule (#20592)
(Nicolas Le Cam)
- 0f12bcd linter/react: Implement `hook-use-state` rule (#20986) (Khaled
Labeb)
- 1513a9f oxlint/lsp: Show note field for lsp diagnostic (#20983)
(Sysix)
- 7fdf722 linter/unicorn: Implement `no-useless-iterator-to-array` rule
(#20945) (Mikhail Baev)
- 39c8f2c linter/jest: Implement padding-around-after-all-blocks
(#21034) (Sapphire)
- ac39e51 linter/eslint-vitest-plugin: Prefer importing vitest globals
(#20960) (Said Atrahouch)
- 0b84de1 oxlint: Support allow option for prefer-promise-reject-errors
(#20934) (camc314)
- 23db851 linter/consistent-return: Move rule from nursery to suspicious
(#20920) (camc314)
- 9a27e32 linter/no-unnecessary-type-conversion: Move rule from nursery
to suspicious (#20919) (camc314)
- 1ca7b58 linter/dot-notation: Move rule from nursery to style (#20918)
(camc314)
- 73ba81a linter/consistent-type-exports: Move rule from nursery to
style (#20917) (camc314)
- b9199b1 linter/unicorn: Implement switch-case-break-position (#20872)
(Mikhail Baev)
- 3435ff8 linter: Implements `prefer-snapshot-hint` rule in Jest and
Vitest (#20870) (Said Atrahouch)
- 98510d2 linter: Implement react/prefer-function-component (#19652)
(Connor Shea)
- 871f9d9 linter: Implement no-useless-assignment (#15466) (Zhaoting
Zhou)
- 0f01fbd linter: Implement eslint/object-shorthand (#17688) (yue)

### 🐛 Bug Fixes

- dd2df87 npm: Export package.json for oxlint and oxfmt (#20784) (kazuya
kawaguchi)
- 9bc77dd linter/no-unused-private-class-members: False positive with
await expr (#21067) (camc314)
- 60a57cd linter/const-comparisons: Detect equality contradictions
(#21065) (camc314)
- 2bb2be2 linter/no-array-index-key: False positive when index is passed
as function argument (#21012) (bab)
- 6492953 linter/no-this-in-sfc: Only flag `this` used as member
expression object (#20961) (bab)
- 9446dcc oxlint/lsp: Skip `node_modules` in oxlint config walker
(#21004) (copilot-swe-agent)
- af89923 linter/no-namespace: Support glob pattern matching against
basename (#21031) (bab)
- 64a1a7e oxlint: Don't search for nested config outside base config
(#21051) (Sysix)
- 3b953bc linter/button-has-type: Ignore `document.createElement` calls
(#21008) (Said Atrahouch)
- 8c36070 linter/unicorn: Add support for `Array.from()` for
`prefer-set-size` rule (#21016) (Mikhail Baev)
- c1a48f0 linter: Detect vitest import from vite-plus/test (#20976)
(Said Atrahouch)
- 5c32fd1 lsp: Prevent corrupted autofix output from overlapping text
edits (#19793) (Peter Wagenet)
- ca79960 linter/no-array-index-key: Move span to `key` property
(#20947) (camc314)
- 2098274 linter: Add suggestion for `jest/prefer-equality-matcher`
(#20925) (eryue0220)
- 6eb77ec linter: Allow default-import barrels in import/named (#20757)
(Bazyli Brzóska)
- 9c218ef linter/eslint-vitest-plugin: Remove pending fix status for
require-local-test-context-for-concurrent-snapshot (#20890) (Said
Atrahouch)

### ⚡ Performance

- fb52383 napi/parser, linter/plugins: Clear buffers and source texts
earlier (#21025) (overlookmotel)
- 3b7dec4 napi/parser, linter/plugins: Use `utf8Slice` for decoding
UTF-8 strings (#21022) (overlookmotel)
- 012c924 napi/parser, linter/plugins: Speed up decoding strings in raw
transfer (#21021) (overlookmotel)
- 55e1e9b napi/parser, linter/plugins: Initialize vars as 0 (#21020)
(overlookmotel)
- c25ef02 napi/parser, linter/plugins: Simplify branch condition in
`deserializeStr` (#21019) (overlookmotel)
- 9f494c3 napi/parser, linter/plugins: Raw transfer use
`String.fromCharCode` in string decoding (#21018) (overlookmotel)
- 0503a78 napi/parser, linter/plugins: Faster deserialization of `raw`
fields (#20923) (overlookmotel)
- a24f75e napi/parser: Optimize string deserialization for non-ASCII
sources (#20834) (Joshua Tuddenham)

### 📚 Documentation

- af72b80 oxlint: Fix typo for --tsconfig (#20889) (leaysgur)
- 70c53b1 linter: Highlight that tsconfig is not respected in type aware
linting (#20884) (camc314)
# Oxfmt
### 🚀 Features

- 35cf6e8 oxfmt: Add node version hint for ts config import failures
(#21046) (camc314)

### 🐛 Bug Fixes

- dd2df87 npm: Export package.json for oxlint and oxfmt (#20784) (kazuya
kawaguchi)
- 9d45511 oxfmt: Propagate file write errors instead of panicking
(#20997) (leaysgur)
- 139ddd9 formatter: Handle leading comment after array elision (#20987)
(leaysgur)
- 4216380 oxfmt: Support `.editorconfig` `tab_width` fallback (#20988)
(leaysgur)
- d10df39 formatter: Resolve pending space in fits measurer before
expanded-mode early exit (#20954) (Dunqing)
- f9ef1bd formatter: Avoid breaking after `=>` when arrow body has JSDoc
type cast (#20857) (bab)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-editor Area - Editor and Language Server A-linter Area - Linter C-bug Category - Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

oxlint/lsp: does not fix code if same 'block' or 'line' has more than 1 jsPlugin rule violation

5 participants