Skip to content

fix(edit): reject an edit whose strings differ only in line ending style - #46849

Open
devYRPauli wants to merge 1 commit into
anomalyco:devfrom
devYRPauli:edit-crlf-identity
Open

devYRPauli wants to merge 1 commit into
anomalyco:devfrom
devYRPauli:edit-crlf-identity

Conversation

@devYRPauli

@devYRPauli devYRPauli commented Sep 2, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #45927

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

packages/core/src/tool/edit.ts rejects an edit where oldString equals newString. That check runs on the raw input at line 127. The tool converts both strings to the file's line ending at lines 163-164, 36 lines later.

Two strings that differ only in line ending style pass the raw check and become the same text after the conversion. source.text.replace(oldString, newString) is then the identity function. countOccurrences still returns 1, so the tool reports success and diffLines produces a patch with no content.

Measured on dev (69c172e), through the real tool, on a CRLF file holding before\r\nrest\r\n, with oldString: "before\r\nrest" and newString: "before\nrest":

result: {"type":"text","value":"Edited file successfully: crlf.txt\nReplacements: 1\n```diff\n-before\n-rest\n+before\n+rest\n```"}
file unchanged: true
writes: 1

The model reads Replacements: 1 and believes the edit landed. An LF file gives the same result with the arguments the other way round, because both strings convert to \n.

This PR runs the same check again after the conversion. The v1 tool already works this way: packages/opencode/src/tool/edit.ts:683 holds the guard inside replace(), and its caller at :129-132 converts both strings before it calls in. Fed the same input, v1 throws No changes to apply: oldString and newString are identical.

After the change the tool returns that error and writes nothing:

result: {"type":"error","value":"No changes to apply: oldString and newString are identical."}
file unchanged: true
writes: 0

The raw check at line 127 stays where it is. It fails before the permission prompt and before the file read, which is still the right place for the case it catches.

How did you verify your code works?

New case in packages/core/test/tool-edit.test.ts, next to the existing preserves BOM and CRLF line endings case. It writes a CRLF file, calls the tool with strings that differ only in line ending style, and asserts the error, the unchanged file content, and zero writes.

bun test test/tool-edit.test.ts                  11 pass, 0 fail
bun test test/tool-edit.test.ts test/tool-write.test.ts \
         test/patch.test.ts test/file-mutation.test.ts    37 pass, 0 fail
bun run typecheck                                30 tasks successful

With the guard removed the new case fails and the other 10 pass, so it tests this fix and nothing else.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

The identity guard runs on the raw input. The tool then converts both strings
to the file's line ending 36 lines later. Strings that differ only in line
ending style therefore pass the guard and become the same text.

The tool reported "Edited file successfully" and "Replacements: 1" with an
empty patch, and the file did not change.

Run the same check again after the conversion. The v1 tool already does this:
packages/opencode/src/tool/edit.ts:683 holds the guard inside replace(), which
its caller reaches with both strings already converted.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on my search results, I found two related PRs that address similar line ending issues in the edit tool:

  1. fix(core): preserve CRLF endings when splicing patch hunks (fix(core): preserve CRLF endings when splicing patch hunks #45931)

  2. fix(core): match LF regions of mixed-ending files in edit (fix(core): match LF regions of mixed-ending files in edit #45888)

These PRs appear to be part of a broader set of fixes addressing CRLF and line ending consistency in the edit tool. While they're not exact duplicates of PR #46849, they're addressing related issues in the same packages/core/src/tool/edit.ts file and the same general problem domain (line ending normalization and validation).

@devYRPauli

Copy link
Copy Markdown
Author

Neither PR covers this one.

#45931 changes packages/core/src/patch.ts. This PR does not touch that file.

#45888 changes detectLineEnding at edit.ts:43, so a mixed-ending file is matched as LF. It repairs a match that fails. This PR adds a guard at edit.ts:165, after convertToLineEnding runs, and repairs an edit that reports Replacements: 1 and writes nothing. The two hunks are about 120 lines apart and do not conflict.

The issues are separate. #45888 closes #45880. This PR closes #45927.

#45888 also does not remove the case here. On a pure CRLF file both strings still convert to CRLF and become identical. On a mixed file both convert to LF and become identical. The tool still reports success in each case, so the guard is still needed.

@devYRPauli

Copy link
Copy Markdown
Author

#47088 fixes the same defect on v2, where the tool lives at packages/core/src/tool/plugin/edit.ts. The file moved between the branches, so this diff does not apply there and that one does not apply here.

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.

edit reports success for a CRLF-only line-ending change that does nothing

1 participant