Description
This is a speculative feature proposal for a new analyzer. I would be sympathetic to an objection that this is too prescriptive, too complex, or too narrow to be included as a hint in gopls. I'd be happy to just implement this downstream for my own use case.
Background
A search of open-source and Google-internal Go code shows that a large majority of custom comparison functions (for slices.SortFunc and friends) fall into one of two buckets:
- ordering by a field value (x < y iff x.Field < y.Field)
- lexicographic ordering (order by a field, but if they're equal, order by a different field, and so on)
(Really, ordering by a field value is a degenerate case of lexicographic ordering.)
There are a variety of ways that code authors express a lexicographic ordering, too many to capture with a straightforward AST analysis (examples below). But most lex ordering implementations can be grouped into a small handful of control-flow patterns.
Go 1.22 introduced cmp.Or, which enables a simple and uniform way to express lexicographic orderings:
cmp.Or(
cmp.Compare(x.Field, y.Field),
cmp.Compare(x.OtherField, y.OtherField)
)
Analyzer idea
There could be an analyzer that identifies a few simple patterns for custom comparisons (as function literals in calls to slices.SortFunc et al.) and suggests simplification using cmp.Or and/or cmp.Compare.
If a simple analysis can't detect a lexicographic ordering, we will not suggest any changes.
Ulterior motive
There are tons of calls to an old version of x/exp/slices in Google code, where orderings are "less"-style (return true iff a < b) instead of "cmp"-style (return an int, so that a comparison a _ b
is written as compare(a, b) _ 0
).
An analyzer that handles a few common patterns for custom comparison functions would allow for conversion of 90+% of calls to x/exp/slices.SortFunc to the standard slices.SortFunc, saving a substantial amount of manual conversion work.
Open questions
- Does Go tooling want to support a migration for users relying on old unstable experimental packages? If not, a more narrowly focused analyzer could still provide value for pre-go1.22 code (and for code where the authors were unaware of cmp.Or or cmp.Compare)
- Would we want to handle certain special comparisons, or leave it simple? (e.g.
using strings.Compare for strings instead of cmp.Compare[per cmd/compile: intrinsify cmp.Compare on common types such as strings #71270 we don't want to do this], or handling time.Compare rather than restricting the analyzer to only look at fields with types satisfying cmp.Ordered) - Trying to preserve comments from the original code would be really complex at best and probably infeasible in practice. We don't like when analyzers remove comments. Should we just refuse to suggest a fix if the function contains any comments?
Examples
// simple field comparison
slices.SortFunc(xs, func(a, b X) int {
if a.Field < b.Field {
return -1
}
if b.Field < a.Field {
return 1
}
return 0
})
// rewrite to:
slices.SortFunc(xs, func(a, b X) int { return cmp.Compare(a.Field, b.Field) })
// lexicographical ordering
slices.SortFunc(xs, func(a, b X) int {
if a.ID < b.ID {
return -1
}
if a.ID > b.ID {
return 1
}
if a.Name < b.Name {
return -1
}
if a.Name > b.Name {
return 1
}
return 0
}
// rewrite to:
slices.SortFunc(xs, func(a, b X) int {
return cmp.Or(cmp.Compare(a.ID, b.ID),
cmp.Compare(a.Name, b.Name))
})
// another lex ordering (in https://cs.opensource.google/go/x/tools/+/master:internal/modindex/modindex.go)
slices.SortFunc(w.newIndex.Entries, func(l, r Entry) int {
if n := strings.Compare(l.PkgName, r.PkgName); n != 0 {
return n
}
return strings.Compare(l.ImportPath, r.ImportPath)
})
// rewrite to:
slices.SortFunc(w.newIndex.Entries, func(l, r Entry) int {
return cmp.Or(strings.Compare(l.PkgName, r.PkgName),
strings.Compare(l.ImportPath, r.ImportPath))
})
// using the old version of x/exp/slices
slices.SortFunc(xs, func(a, b X) bool { return a.Field1 < b.Field1 }
slices.SortFunc(ys, func(a, b Y) bool {
if a.Field1 != b.Field1 {
return a.Field1 < b.Field1
}
return a.Field2 < b.Field2
})
// import standard slices package and rewrite to:
slices.SortFunc(xs, func(a, b X) int { return cmp.Compare(a.Field1, b.Field1) }
slices.SortFunc(xs, func(a, b X) int {
return cmp.Or(cmp.Compare(a.Field1, b.Field1),
cmp.Compare(a.Field2, b.Field2))
})
cc: @adonovan