Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions goldens/by_regex.err
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
WRN while parsing option "by_regex": error parsing regexp: missing argument to repetition operator: `*` line=85
WRN by_regex cannot be used with ignore_prefixes (consider adding a non-capturing group to the start of your regex instead of ignore_prefixes: "(?:foo|bar)") line=92
WRN while parsing option "by_regex": error parsing regexp: missing argument to repetition operator: `*` line=93
WRN by_regex cannot be used with ignore_prefixes (consider adding a non-capturing group to the start of your regex instead of ignore_prefixes: "(?:foo|bar)") line=100
exit status 1
8 changes: 8 additions & 0 deletions goldens/by_regex.in
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ Multiple regexes
int bar
keep-sorted-test end

One regex, multiple capturing groups
keep-sorted-test start by_regex=(.*)_(.*)
foo_bar
foo_baz
bar_baz
baz_qux
keep-sorted-test end

Multiline blocks
keep-sorted-test start block=yes newline_separated=yes by_regex=(\w+)\(\)\s+{
bool func2() {
Expand Down
8 changes: 8 additions & 0 deletions goldens/by_regex.out
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ Multiple regexes
long foo
keep-sorted-test end

One regex, multiple capturing groups
keep-sorted-test start by_regex=(.*)_(.*)
bar_baz
baz_qux
foo_bar
foo_baz
keep-sorted-test end

Multiline blocks
keep-sorted-test start block=yes newline_separated=yes by_regex=(\w+)\(\)\s+{
List<SomeReallyLongTypeParameterThatWouldForceTheFunctionNameOntoTheNextLine>
Expand Down
109 changes: 22 additions & 87 deletions keepsorted/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,14 @@ func (b block) sorted() (sorted []string, alreadySorted bool) {
}
}

log.Printf("Creating line groups for block at index %d (options %v)", b.start, b.metadata.opts)
groups := groupLines(lines, b.metadata)
log.Printf("Previous %d groups were for block at index %d are (options %v)", len(groups), b.start, b.metadata.opts)
trimTrailingComma := handleTrailingComma(groups)

wasNewlineSeparated := true
if b.metadata.opts.NewlineSeparated {
wasNewlineSeparated = isNewlineSeparated(groups)
var withoutNewlines []lineGroup
var withoutNewlines []*lineGroup
for _, lg := range groups {
if isNewline(lg) {
continue
Expand All @@ -255,9 +255,9 @@ func (b block) sorted() (sorted []string, alreadySorted bool) {
removedDuplicate := false
if b.metadata.opts.RemoveDuplicates {
seen := map[string]bool{}
var deduped []lineGroup
var deduped []*lineGroup
for _, lg := range groups {
if s := lg.joinedLines() + "\n" + strings.Join(lg.comment, "\n"); !seen[s] {
if s := lg.String(); !seen[s] {
seen[s] = true
deduped = append(deduped, lg)
} else {
Expand All @@ -267,20 +267,25 @@ func (b block) sorted() (sorted []string, alreadySorted bool) {
groups = deduped
}

less := b.lessFn()

if alreadySorted && wasNewlineSeparated && !removedDuplicate && slices.IsSortedFunc(groups, less) {
if alreadySorted && wasNewlineSeparated && !removedDuplicate && slices.IsSortedFunc(groups, compareLineGroups) {
log.Printf("It was already sorted!")
for _, lg := range groups {
log.Printf("%#v", lg)
}
trimTrailingComma(groups)
return lines, true
}

slices.SortStableFunc(groups, less)
slices.SortStableFunc(groups, compareLineGroups)
for _, lg := range groups {
log.Printf("%#v", lg)
}

trimTrailingComma(groups)

if b.metadata.opts.NewlineSeparated {
var separated []lineGroup
newline := lineGroup{lines: []string{""}}
var separated []*lineGroup
newline := &lineGroup{lineGroupContent: lineGroupContent{lines: []string{""}}}
for _, lg := range groups {
if separated != nil {
separated = append(separated, newline)
Expand Down Expand Up @@ -308,7 +313,7 @@ func (b block) sorted() (sorted []string, alreadySorted bool) {
// .
// .
// non-empty group
func isNewlineSeparated(gs []lineGroup) bool {
func isNewlineSeparated(gs []*lineGroup) bool {
if len(gs) == 0 {
return true
}
Expand All @@ -325,15 +330,15 @@ func isNewlineSeparated(gs []lineGroup) bool {
}

// isNewline determines if lg is just an empty line.
func isNewline(lg lineGroup) bool {
func isNewline(lg *lineGroup) bool {
return len(lg.comment) == 0 && len(lg.lines) == 1 && strings.TrimSpace(lg.lines[0]) == ""
}

// handleTrailingComma handles the special case that all lines of a sorted segment are terminated
// by a comma except for the final element; in this case, we add a ',' to the
// last linegroup and strip it again after sorting.
func handleTrailingComma(lgs []lineGroup) (trimTrailingComma func([]lineGroup)) {
var dataGroups []lineGroup
func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup)) {
var dataGroups []*lineGroup
for _, lg := range lgs {
if len(lg.lines) > 0 {
dataGroups = append(dataGroups, lg)
Expand All @@ -343,7 +348,7 @@ func handleTrailingComma(lgs []lineGroup) (trimTrailingComma func([]lineGroup))
if n := len(dataGroups); n > 1 && allHaveSuffix(dataGroups[0:n-1], ",") && !dataGroups[n-1].hasSuffix(",") {
dataGroups[n-1].append(",")

return func(lgs []lineGroup) {
return func(lgs []*lineGroup) {
for i := len(lgs) - 1; i >= 0; i-- {
if len(lgs[i].lines) > 0 {
lgs[i].trimSuffix(",")
Expand All @@ -353,84 +358,14 @@ func handleTrailingComma(lgs []lineGroup) (trimTrailingComma func([]lineGroup))
}
}

return func([]lineGroup) {}
return func([]*lineGroup) {}
}

func allHaveSuffix(lgs []lineGroup, s string) bool {
func allHaveSuffix(lgs []*lineGroup, s string) bool {
for _, lg := range lgs {
if !lg.hasSuffix(s) {
return false
}
}
return true
}

func (b block) lessFn() cmpFunc[lineGroup] {
// Always put groups that are only comments last.
commentOnlyBlock := comparing(func(lg lineGroup) int {
if len(lg.lines) > 0 {
return 0
}
return 1
})

regexTransform := func(lg lineGroup) []regexToken {
return b.metadata.opts.regexTransform(lg.joinedLines())
}

// Assign a weight to each prefix so that they will be sorted into their
// predetermined order.
// Weights are negative so that entries with matching prefixes are put before
// any non-matching line (which will have a weight of 0).
//
// An empty prefix can be used to move "non-matching" entries to a position
// between other prefixes.
prefixWeights := make(map[string]int)
for i, p := range b.metadata.opts.PrefixOrder {
prefixWeights[p] = i - len(b.metadata.opts.PrefixOrder)
}
// Sort prefixes longest -> shortest to find the most appropriate weight.
longestFirst := comparing(func(s string) int { return len(s) }).reversed()
prefixes := slices.SortedStableFunc(slices.Values(b.metadata.opts.PrefixOrder), longestFirst)

prefixOrder := comparing(func(s []string) int {
if len(s) == 0 {
return 0
}
p, ok := b.metadata.opts.hasPrefix(s[0], slices.Values(prefixes))
if !ok {
return 0
}
return prefixWeights[p]
})

// Combinations of switches (for example, case-insensitive and numeric
// ordering) which must be applied to create a single comparison key,
// otherwise a sub-ordering can preempt a total ordering:
// Foo_45
// foo_123
// foo_6
// would be sorted as either (numeric but not case-insensitive)
// Foo_45
// foo_6
// foo_123
// or (case-insensitive but not numeric)
// foo_123
// Foo_45
// foo_6
// but should be (case-insensitive and numeric)
// foo_6
// Foo_45
// foo_123
transformOrder := comparingFunc(func(s string) numericTokens {
s = b.metadata.opts.trimIgnorePrefix(s)
if !b.metadata.opts.CaseSensitive {
s = strings.ToLower(s)
}
return b.metadata.opts.maybeParseNumeric(s)
}, numericTokens.compare)

return commentOnlyBlock.
andThen(comparingFunc(regexTransform, compareRegexTokens(prefixOrder.andThen(lexicographically(transformOrder))))).
andThen(lineGroup.less)
}
Loading
Loading