Skip to content

[Bug]: multi_patch corrupts file when sequential edits shift byte offsets #3249

@tusharmath

Description

@tusharmath

Bug Description

multi_patch corrupts a file when sequential edits shift byte offsets unexpectedly. The second (and later) edits can match the wrong position and merge with surrounding lines, producing syntactically invalid output.

Steps to Reproduce

  1. A file contains two regions to patch — the first region includes adjacent content (e.g. type aliases) that is not part of old_string but happens to be deleted by the replacement.
  2. Call multi_patch with two edits in the same call:
    • Edit 1: Replace a struct definition block.
    • Edit 2: Replace a single line that appears later in the file inside an async move { … } block.
  3. Observe that Edit 1 silently removes extra lines (type alias declarations) that precede the matched struct, shifting subsequent line positions.
  4. Edit 2 then finds its old_string at the wrong byte offset and stitches the replacement to the tail of the preceding line instead of replacing only the target line.

Concrete example from a real session:

Edit 2 old_string:

                        let start = this.started_at;

Edit 2 new_string:

                        let start = std::time::Instant::now();

Resulting corrupted line in the file:

let handle = tokio::spawn(async mov                        let start = std::time::Instant::now();

The closing e { of async move { was consumed and the newline was dropped, merging both lines into one invalid token sequence.

Expected Behavior

Each edit in multi_patch should operate on the result of the previous edit without corrupting surrounding context. If Edit 1 changes the file, Edit 2 should find its old_string correctly in the updated content and replace only that string — preserving adjacent characters, newlines, and indentation.

Actual Behavior

When an earlier edit removes more content than strictly matched by old_string (e.g. trailing/leading whitespace or adjacent declarations get swept up), the byte-offset used by the next edit is wrong. The tool inserts the replacement at the wrong position, truncating the preceding line and joining it with the replacement text without a newline separator.

Using undo on the file confirmed the corruption: restoring the file added back both the silently-deleted type aliases and fixed the merged tokio::spawn line, proving the root cause is cascading offset corruption between sequential edits.

Environment

  • Tool: multi_patch
  • File type: Rust (.rs)
  • Number of edits in one call: 2
  • Edit 1 scope: multi-line struct + impl block replacement
  • Edit 2 scope: single-line replacement inside a nested async closure

Metadata

Metadata

Labels

severity: highSignificant impact; core functionality is impaired.type: bugSomething isn't working.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions