Skip to content

Commit 56367b5

Browse files
committed
fix(syncmodutil): parse single-line replaces and tokenize strip logic
Two follow-ups to #983 review feedback: - getReplacedVersionsFromString now also picks up single-line `replace foo [v] => bar [v]` directives in the source go.mod, not just block form. Without this, a source that uses single-line replaces loses them in the emitted output, violating the documented guarantee that source-declared replaces are mirrored verbatim. Block boundaries are tracked so lines inside `replace (...)` are only processed once (by the existing block parser). - stripConflictingReplaces tokenizes with strings.Fields instead of a fixed "replace " prefix. go.mod permits arbitrary whitespace after the keyword (e.g. `replace<tab>foo => bar`), and such lines were previously not stripped, which would cause `go mod tidy` to fail with "multiple replacements for <module>" once the new pin block was appended. Signed-off-by: Yi Nuo <218099172+yi-nuo426@users.noreply.github.com>
1 parent 24f97a9 commit 56367b5

1 file changed

Lines changed: 41 additions & 4 deletions

File tree

  • cmd/syncmodutil/internal/modsync

cmd/syncmodutil/internal/modsync/sync.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,17 @@ func stripConflictingReplaces(data []string, conflicts map[string]bool) []string
170170
switch {
171171
case inReplaceBlock:
172172
rest = trim
173-
case strings.HasPrefix(trim, "replace "):
174-
rest = strings.TrimPrefix(trim, "replace ")
175173
default:
176-
out = append(out, line)
177-
continue
174+
// go.mod allows arbitrary whitespace after the `replace`
175+
// keyword (e.g. `replace\tfoo => bar`), so tokenize rather
176+
// than match a fixed "replace " prefix. Skip any other line
177+
// containing "=>" (e.g. an in-comment arrow).
178+
fields := strings.Fields(trim)
179+
if len(fields) == 0 || fields[0] != "replace" {
180+
out = append(out, line)
181+
continue
182+
}
183+
rest = strings.TrimSpace(strings.TrimPrefix(trim, fields[0]))
178184
}
179185
parts := strings.SplitN(rest, "=>", 2)
180186
from := strings.Fields(strings.TrimSpace(parts[0]))
@@ -252,6 +258,7 @@ func getRequiredVersionsFromString(s string) (p []Package) {
252258
return p
253259
}
254260
func getReplacedVersionsFromString(s string) (p [][]Package) {
261+
// Block form: replace ( ... )
255262
reps := ReplacePatternRegex.FindAllString(s, -1)
256263
for _, req := range reps {
257264
data := getStringWithinCharacters(req, '(', ')')
@@ -268,6 +275,36 @@ func getReplacedVersionsFromString(s string) (p [][]Package) {
268275
}
269276
}
270277
}
278+
279+
// Single-line form: `replace foo [v] => bar [v]` outside any block.
280+
// go.mod allows arbitrary whitespace after the `replace` keyword, so
281+
// tokenize rather than match a fixed prefix.
282+
inBlock := false
283+
for _, line := range strings.Split(s, "\n") {
284+
trim := strings.TrimSpace(line)
285+
if !inBlock && (trim == "replace (" || trim == "replace(") {
286+
inBlock = true
287+
continue
288+
}
289+
if inBlock {
290+
if trim == ")" {
291+
inBlock = false
292+
}
293+
continue
294+
}
295+
if !strings.Contains(trim, "=>") {
296+
continue
297+
}
298+
fields := strings.Fields(trim)
299+
if len(fields) == 0 || fields[0] != "replace" {
300+
continue
301+
}
302+
rest := strings.TrimSpace(strings.TrimPrefix(trim, fields[0]))
303+
p0 := getPackagesAndVersionsFromPackageVersions(rest)
304+
if len(p0) != 0 {
305+
p = append(p, p0)
306+
}
307+
}
271308
return p
272309
}
273310
func getPackagesAndVersionsFromPackageVersions(pkg string) (p []Package) {

0 commit comments

Comments
 (0)