modfile: use semantic sort for exclude blocks For golang/go#60028. Change-Id: I4c7a726a900fc7c4b34816eba5cfd0361c45315f Reviewed-on: https://go-review.googlesource.com/c/mod/+/492990 Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> Reviewed-by: Bryan Mills <bcmills@google.com>
diff --git a/modfile/rule.go b/modfile/rule.go index 6bcde8f..c20aef1 100644 --- a/modfile/rule.go +++ b/modfile/rule.go
@@ -1387,13 +1387,21 @@ func (f *File) SortBlocks() { f.removeDups() // otherwise sorting is unsafe + // semanticSortForExcludeVersionV is the Go version (plus leading "v") at which + // lines in exclude blocks start to use semantic sort instead of lexicographic sort. + // See go.dev/issue/60028. + const semanticSortForExcludeVersionV = "v1.21" + useSemanticSortForExclude := f.Go != nil && semver.Compare("v"+f.Go.Version, semanticSortForExcludeVersionV) >= 0 + for _, stmt := range f.Syntax.Stmt { block, ok := stmt.(*LineBlock) if !ok { continue } less := lineLess - if block.Token[0] == "retract" { + if block.Token[0] == "exclude" && useSemanticSortForExclude { + less = lineExcludeLess + } else if block.Token[0] == "retract" { less = lineRetractLess } sort.SliceStable(block.Line, func(i, j int) bool { @@ -1496,6 +1504,22 @@ return len(li.Token) < len(lj.Token) } +// lineExcludeLess reports whether li should be sorted before lj for lines in +// an "exclude" block. +func lineExcludeLess(li, lj *Line) bool { + if len(li.Token) != 2 || len(lj.Token) != 2 { + // Not a known exclude specification. + // Fall back to sorting lexicographically. + return lineLess(li, lj) + } + // An exclude specification has two tokens: ModulePath and Version. + // Compare module path by string order and version by semver rules. + if pi, pj := li.Token[0], lj.Token[0]; pi != pj { + return pi < pj + } + return semver.Compare(li.Token[1], lj.Token[1]) < 0 +} + // lineRetractLess returns whether li should be sorted before lj for lines in // a "retract" block. It treats each line as a version interval. Single versions // are compared as if they were intervals with the same low and high version.
diff --git a/modfile/rule_test.go b/modfile/rule_test.go index 26879fb..f8dd174 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go
@@ -1224,6 +1224,52 @@ `, false, }, + // Exclude blocks are sorted using semver in ascending order + // in go.mod files that opt in to Go version 1.21 or newer. + { + `sort_exclude_go121_semver`, + `module m + go 1.21 + exclude ( + b.example/m v0.9.0 + a.example/m v1.0.0 + b.example/m v0.10.0 + c.example/m v1.1.0 + b.example/m v0.11.0 + )`, + `module m + go 1.21 + exclude ( + a.example/m v1.0.0 + b.example/m v0.9.0 + b.example/m v0.10.0 + b.example/m v0.11.0 + c.example/m v1.1.0 + ) + `, + true, + }, + { + `sort_exclude_!go121_lexicographically`, // Maintain the previous (less featureful) behavior to avoid unnecessary churn. + `module m + exclude ( + b.example/m v0.9.0 + a.example/m v1.0.0 + b.example/m v0.10.0 + c.example/m v1.1.0 + b.example/m v0.11.0 + )`, + `module m + exclude ( + a.example/m v1.0.0 + b.example/m v0.10.0 + b.example/m v0.11.0 + b.example/m v0.9.0 + c.example/m v1.1.0 + ) + `, + true, + }, } var addRetractValidateVersionTests = []struct {