Skip to content

Fix content leakage in applyPatch when trailing newline counts differ#4668

Open
ShehabSherif0 wants to merge 3 commits intomicrosoft:mainfrom
ShehabSherif0:fix/apply-patch-trailing-newline-leak
Open

Fix content leakage in applyPatch when trailing newline counts differ#4668
ShehabSherif0 wants to merge 3 commits intomicrosoft:mainfrom
ShehabSherif0:fix/apply-patch-trailing-newline-leak

Conversation

@ShehabSherif0
Copy link

@ShehabSherif0 ShehabSherif0 commented Mar 24, 2026

Fixes #4667

Problem

generateUpdateTextDocumentEdit in applyPatchTool.tsx uses lines.length (from the new content) as the replace range end instead of covering the entire original document. When the AI shortens a file and the trailing newline count differs between old and new content, original lines leak into the result.

Root cause

The original code applies three separate TextEdits (all on original positions, atomically):

  1. Replace Range(0,0)-(lines.length, 0) with newContent
  2. Insert trailing \n at Position(lines.length + i, 0) for each missing trailing line
  3. Delete Range(newLineCount, 0)-(textDocument.lineCount, 0)

When originalTrailing > newTrailing, a gap of (originalTrailing - newTrailing) original lines exists between the replace end and the delete start. These lines are neither replaced nor deleted.

Example

Original: "ALPHA\nBETA\nGAMMA\n" (lineCount=4, trailing=1). AI removes BETA and GAMMA: newContent = "ALPHA" (trailing=0).

  • Replace covers line 0 only (Position(0,0) to Position(1,0))
  • Insert adds \n at Position(1,0)
  • Delete covers lines 2-3 (Position(2,0) to Position(4,0))

Original line 1 ("BETA") is in the gap, untouched by any edit. Result: "ALPHA\nBETA\n" instead of "ALPHA\n".

When it triggers

  1. AI shortens a file (common during refactoring)
  2. Trailing newline counts differ between original and new content (common when the LLM omits the trailing newline)

Cascading impact

turnEditedDocuments stores the expected content, creating a divergence between the AI's view and the actual file state. Subsequent edits then operate on wrong assumptions.

Fix

Build adjustedContent with trailing newlines appended, then replace the entire original document in a single operation:

const extraTrailing = Math.max(originalTrailing - newTrailing, 0);
const adjustedContent = extraTrailing > 0 ? newContent + '\n'.repeat(extraTrailing) : newContent;

workspaceEdit.replace(path, new Range(
    new Position(0, 0),
    new Position(textDocument.lineCount, 0)  // covers entire original
), adjustedContent);

This replaces the prior three-operation approach (replace + insert loop + conditional delete) with a single replace that covers the full document. No gap is possible.

This matches the correct pattern already used in codeMapper.ts.

Testing

  • Both existing and new tests pass (2/2 in applyPatch.spec.tsx)
  • New regression test reproduces the exact leak scenario (original "ALPHA\nBETA\nGAMMA\n", AI produces "ALPHA", asserts result is "ALPHA\n" not "ALPHA\nBETA\n")

…ewline counts differ

The replace range used lines.length (new content line count) instead of
textDocument.lineCount (original), leaving a gap of original lines between
the replace end and the delete start when originalTrailing > newTrailing.

Fix: Build adjusted content with trailing newlines appended, then replace
the entire original document in a single operation. This matches the
correct pattern already used in codeMapper.ts.

Fixes microsoft#4667
Copy link
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

Fixes a correctness issue in the apply_patch tool where shortening a file could leave (“leak”) original trailing lines in the final document when the original and new contents have different trailing newline counts.

Changes:

  • Replace the entire original text document range (based on the original document’s lineCount) instead of only replacing up to the new content’s line count.
  • Normalize the replacement content by appending trailing newlines to match the original document’s trailing empty-line count, avoiding separate insert/delete edits.

- Add test: verify original content does not leak when trailing newline
  counts differ (the AI shortens file, LLM omits trailing newline)
- Optimize: use '\n'.repeat() instead of loop for trailing adjustment
Copy link
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

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

Use ALPHA/BETA/GAMMA instead of A/B/C in the trailing newline regression test. Short single-character content triggered the parser edit-distance fuzzy matching at the wrong position, causing false test failures unrelated to the fix under test. Also add CRLF normalization when reading the fixture for cross-platform safety.
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.

applyPatchTool: original content leaks through when trailing newline counts differ

3 participants