Repository navigation
x/tools/gopls: unified diff has a wrong new-file start line in hunks after the first #81453
Description
gopls version
golang.org/x/tools/gopls v0.23.0 (latest release), go1.27.1 darwin/arm64, macOS 26.6.2.
What did you do?
$ cat go.mod
module repro
go 1.21
$ cat -n p.go
1 package p
2
3 type T struct{}
4
5 func (t T) Range() {}
6
7 func a(t T) { t.Range() }
8 // filler 1
9 // filler 2
10 // filler 3
11 // filler 4
12 // filler 5
13 // filler 6
14 // filler 7
15 // filler 8
16 func b(t T) { t.Range() }
$ gopls rename -d p.go:5:12 Each
What did you expect to see?
A rename changes no line counts, so every hunk header must have the same start line on both sides. Applying the same rename with gopls rename -w and diffing with GNU diff gives exactly that:
@@ -2,9 +2,9 @@
...
@@ -13,4 +13,4 @@
What did you see instead?
The second hunk's new-file start line is wrong. Everything else in the two diffs is byte-identical:
@@ -2,9 +2,9 @@
type T struct{}
-func (t T) Range() {}
+func (t T) Each() {}
-func a(t T) { t.Range() }
+func a(t T) { t.Each() }
// filler 1
// filler 2
// filler 3
@@ -13,4 +12,4 @@ <-- GNU diff says @@ -13,4 +13,4 @@
// filler 6
// filler 7
// filler 8
-func b(t T) { t.Range() }
+func b(t T) { t.Each() }
The error is not a constant off-by-one
It grows with the size of the preceding hunks. Widening the gap between the two edits inside the first hunk moves the second hunk's old start while its new start stays frozen:
| filler lines between the first two edits | headers printed by gopls |
|---|---|
| 0 | @@ -2,12 +2,12 @@ @@ -20,4 +17,4 @@ |
| 2 | @@ -2,12 +2,12 @@ @@ -20,4 +17,4 @@ |
| 4 | @@ -2,14 +2,14 @@ @@ -22,4 +17,4 @@ |
| 6 | @@ -2,16 +2,16 @@ @@ -24,4 +17,4 @@ |
On a real repository the error is larger. In golang.org/x/tools at v0.50.0, renaming the method Range on (go/callgraph/vta/internal/trie).Map to Each prints, for trie.go:
@@ -68,17 +68,17 @@
@@ -102,7 +94,7 @@ <-- off by 8
It is not specific to rename
gopls format -d prints the same kind of wrong header, so this looks like the shared unified-diff formatter (internal/diff) rather than the rename code path. On a file with two unformatted regions 12 lines apart:
@@ -1,9 +1,10 @@
@@ -17,6 +16,6 @@ <-- the first hunk adds a line, so this should be @@ -17,6 +18,6 @@
Here the header is wrong in the other direction, which is why the rename case above is the clearer reproducer: with no line-count changes the expected value is unambiguous.
Why this matters beyond cosmetics
GNU patch tolerates a wrong new-file start line, so this stays invisible in manual use. It does not stay invisible for programmatic consumers. The MCP tool go_rename_symbol returns this diff as its entire result and writes nothing to disk, so an MCP client has to apply the diff itself; a client that trusts the + start line places the edit in the wrong location. I ran into it while measuring exactly that: three agent runs each ended up re-deriving the rename as textual replacements instead of applying the returned diff, and one of them corrupted a string literal in the process.
Related, and lower priority
The same diffs use absolute paths with no a/ b/ prefixes, so git apply rejects them outright (error: invalid path '/private/tmp/.../p.go') regardless of the hunk headers. That may well be intended for human-readable CLI output, but for go_rename_symbol, whose output exists to be applied by a program, a patch-compatible form (or, better, a structured edit list) would be more useful than prose plus a diff.
Activity
ajvengo commentedon Sep 10, 2026
Cause
toUnifiedkeepstoLineas the running new-file line counter, but the branch that joins a nearby edit into the current hunk adds the intervening lines to the hunk without advancing it (internal/diff/unified.go, current master):The
defaultpath advancestoLinebystart - last, and each inserted line advances it by one, so those two are accounted for. The joiners are not. Every joined edit therefore leavestoLineshort by exactly the number of joiner lines, and the error is carried into the header of every subsequent hunk.That predicts the magnitudes in the report. In the small reproducer the two edits are joined across one blank line, so the second hunk is off by 1. In
x/toolsthe edits intrie.goare at lines 71, 78 and 81, joined across 6 and 2 lines, so the next hunk is off by 8 —@@ -102,7 +94,7 @@.Minimal fix
addEqualLinesalready returns the number of lines it appended, and it is already used that way for the leading edge of a hunk just below, so the fix needs nothing new.Verification
With that one line changed:
@@ -13,4 +13,4 @@, matching GNU diff byte for byte;gopls format -dcase prints@@ -17,6 +18,6 @@, again matchinggofmt+ GNUdiff -u(note the released binary is wrong in the other direction here,+16);x/toolsrename prints@@ -102,7 +102,7 @@instead of+94;go test ./internal/...is green,internal/diff,internal/diff/difftestandinternal/diff/lcsincluded.All four checks were run against a
goplsbuilt from the patched tree, sincegopls/go.modreplacesgolang.org/x/toolswith...A test that would have caught it
The existing tests compare against golden strings, which is why a wrong header survived. A property is cheap here: every hunk header must point at the real position of its own lines — the old lines at
fromLinein the old file, the new lines attoLinein the new file. On 400 randomly generated line-edit pairs (delete, change, insert) that check fails 112 times before the fix and 0 times after.I am happy to send this as a CL (the one-line fix plus the golden-string test for the reproducer and the property test) if that is welcome.
gabyhelp commentedon Sep 10, 2026
Related Issues
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
pjweinb commentedon Sep 10, 2026
Sure. Fix it.
shuaixr commentedon Sep 18, 2026
I'd happy to take a look at this if no one's working on it
gopherbot commentedon Sep 18, 2026
Change https://go.dev/cl/834825 mentions this issue:
cmd/vendor/golang.org/x/tools: fix wrong new-file start line in unified diff