From 4c893821761e6984ffc6976b51091fc8f76ae03b Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 11:01:33 -0700 Subject: [PATCH 1/7] Handle trailing commas with comments For lines/sequences of blocks which appear to be a comma-delimited list, do not consider comments when performing this check. Also, insert commas before comments to avoid mangling list. I kept the match/replace logic in lineGroup, as hasSuffix/trimSuffix were there. This approach does have a few weaknesses. It doesn't properly handle sorting what appear to be quoted comments, e.g.: - "a, #", - "b, //", - c It also doesn't properly handle commented commas: - a, # some comment, # more comment - b, # another comment, # a comment It is better at handling the desired cases, and the above counterexamples where it fails feel more like less-common edge cases. Lastly, it assumes "#" and "//" are always comments, and doesn't allow for alternatives. Handling the above would need quite a bit more work - likely parsing the lines themselves and adding list_delimiter and/or comment_delimiter options. --- goldens/trailing_commas.out | 16 ++++++++-------- keepsorted/block.go | 20 +++++++++++++++----- keepsorted/line_group.go | 13 +++++++++++-- keepsorted/options.go | 2 +- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/goldens/trailing_commas.out b/goldens/trailing_commas.out index 749e403..390d1b0 100644 --- a/goldens/trailing_commas.out +++ b/goldens/trailing_commas.out @@ -1,8 +1,8 @@ Trailing comma except for last keep-sorted-test start 1, - 2, - 3 + 2, + 3 keep-sorted-test end @@ -10,22 +10,22 @@ Trailing comma and a comment TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, - 2 - 3, # three + 2, + 3 # three keep-sorted-test end Trailing comma except for last, last has a comment TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, - 2 # two, - 3 + 2, # two + 3 keep-sorted-test end Trailing comma and a comment, last has a comment TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, - 2 # two - 3, # three + 2, # two + 3 # three keep-sorted-test end diff --git a/keepsorted/block.go b/keepsorted/block.go index 9ff284b..a4be5b9 100644 --- a/keepsorted/block.go +++ b/keepsorted/block.go @@ -16,6 +16,7 @@ package keepsorted import ( "fmt" + "regexp" "slices" "strings" @@ -389,13 +390,13 @@ 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(",") + if n := len(dataGroups); n > 1 && allEndInComma(dataGroups[0:n-1]) && !endInComma(dataGroups[n-1]) { + dataGroups[n-1].replaceSuffix(possibleCommentLineEnd, ", $2") return func(lgs []*lineGroup) { for i := len(lgs) - 1; i >= 0; i-- { if len(lgs[i].lines) > 0 { - lgs[i].trimSuffix(",") + lgs[i].replaceSuffix(commaLineEnd, " $2") return } } @@ -405,11 +406,20 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) return func([]*lineGroup) {} } -func allHaveSuffix(lgs []*lineGroup, s string) bool { +func allEndInComma(lgs []*lineGroup) bool { for _, lg := range lgs { - if !lg.hasSuffix(s) { + if !endInComma(lg) { return false } } return true } + +var ( + commaLineEnd = regexp.MustCompile("(,)(\\s*(#|//).*)?$") + possibleCommentLineEnd = regexp.MustCompile("( +)?((#|//).*)?$") +) + +func endInComma(lg *lineGroup) bool { + return lg.matchesSuffix(commaLineEnd) +} diff --git a/keepsorted/line_group.go b/keepsorted/line_group.go index a637de8..ca26cf9 100644 --- a/keepsorted/line_group.go +++ b/keepsorted/line_group.go @@ -61,7 +61,7 @@ type accessRecorder struct { joinedComment bool } -// matchesAnyRegex returns true if s matches one of the regexes. +// matchesAnyRegex returns true if s matchesSuffix one of the regexes. func matchesAnyRegex(s string, regexes []*regexp.Regexp) bool { for _, regex := range regexes { if regex.FindStringSubmatch(s) != nil { @@ -119,7 +119,7 @@ func groupLines(lines []string, metadata blockMetadata) []*lineGroup { // Returns another boolean indicating whether the group should be ending // after that line if so. shouldAddToRegexDelimitedGroup := func(l string) (addToGroup bool, finishGroupAfter bool) { - if metadata.opts.GroupStartRegex != nil { + if metadata.opts.GroupStartRegex != nil { // For GroupStartRegex, all non-regex-matching lines should be // part of the group including prior lines. return !matchesAnyRegex(l, metadata.opts.GroupStartRegex), false @@ -408,11 +408,20 @@ func (lg *lineGroup) hasSuffix(s string) bool { return len(lg.lines) > 0 && strings.HasSuffix(lg.lines[len(lg.lines)-1], s) } +func (lg *lineGroup) matchesSuffix(re *regexp.Regexp) bool { + return len(lg.lines) > 0 && re.MatchString(lg.lines[len(lg.lines)-1]) +} + func (lg *lineGroup) trimSuffix(s string) { lg.access = accessRecorder{} lg.lines[len(lg.lines)-1] = strings.TrimSuffix(lg.lines[len(lg.lines)-1], s) } +func (lg *lineGroup) replaceSuffix(re *regexp.Regexp, replacement string) { + lg.access = accessRecorder{} + lg.lines[len(lg.lines)-1] = re.ReplaceAllString(lg.lines[len(lg.lines)-1], replacement) +} + func (lg *lineGroup) commentOnly() bool { lg.access.commentOnly = true return len(lg.lines) == 0 diff --git a/keepsorted/options.go b/keepsorted/options.go index 6b5c6fa..45b84a5 100644 --- a/keepsorted/options.go +++ b/keepsorted/options.go @@ -453,7 +453,7 @@ func (opts blockOptions) hasGroupPrefix(s string) bool { } // trimIgnorePrefix removes the first matching IgnorePrefixes from s, if s -// matches one of the IgnorePrefixes. +// matchesSuffix one of the IgnorePrefixes. func (opts blockOptions) trimIgnorePrefix(s string) string { _, s, _ = opts.cutFirstPrefix(s, slices.Values(opts.IgnorePrefixes)) return s From 35b3fd6f44b9486854b01159e1fb8a44b84dbffa Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 12:07:58 -0700 Subject: [PATCH 2/7] Keep comment spacing consistent Comment spacing in lists is now handled consistently. Comments begin one space character after the comma delimiter, except for the terminal element, which has no comma and instead has two spaces before any comment. --- goldens/trailing_commas.out | 8 ++++---- keepsorted/block.go | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/goldens/trailing_commas.out b/goldens/trailing_commas.out index 390d1b0..f73bf12 100644 --- a/goldens/trailing_commas.out +++ b/goldens/trailing_commas.out @@ -1,8 +1,8 @@ Trailing comma except for last keep-sorted-test start 1, - 2, - 3 + 2, + 3 keep-sorted-test end @@ -10,7 +10,7 @@ Trailing comma and a comment TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, - 2, + 2, 3 # three keep-sorted-test end @@ -19,7 +19,7 @@ TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, 2, # two - 3 + 3 keep-sorted-test end Trailing comma and a comment, last has a comment diff --git a/keepsorted/block.go b/keepsorted/block.go index a4be5b9..060289b 100644 --- a/keepsorted/block.go +++ b/keepsorted/block.go @@ -391,12 +391,20 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) } if n := len(dataGroups); n > 1 && allEndInComma(dataGroups[0:n-1]) && !endInComma(dataGroups[n-1]) { - dataGroups[n-1].replaceSuffix(possibleCommentLineEnd, ", $2") + if dataGroups[n-1].matchesSuffix(commentLineEnd) { + dataGroups[n-1].replaceSuffix(commentLineEnd, ", $2") + } else { + dataGroups[n-1].append(",") + } return func(lgs []*lineGroup) { for i := len(lgs) - 1; i >= 0; i-- { if len(lgs[i].lines) > 0 { - lgs[i].replaceSuffix(commaLineEnd, " $2") + if lgs[i].matchesSuffix(commentLineEnd) { + lgs[i].replaceSuffix(commaLineEnd, " $3") + } else { + lgs[i].trimSuffix(",") + } return } } @@ -416,8 +424,8 @@ func allEndInComma(lgs []*lineGroup) bool { } var ( - commaLineEnd = regexp.MustCompile("(,)(\\s*(#|//).*)?$") - possibleCommentLineEnd = regexp.MustCompile("( +)?((#|//).*)?$") + commaLineEnd = regexp.MustCompile("(,)( *)((#|//).*)?$") + commentLineEnd = regexp.MustCompile("( *)((#|//).*)$") ) func endInComma(lg *lineGroup) bool { From 0a96ebb051aa60f92291e2f70dad225121d9a2ea Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 12:11:03 -0700 Subject: [PATCH 3/7] Remove unnecessary function endsInComma was unnecessary since it's handled by a lineGroup method. --- keepsorted/block.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/keepsorted/block.go b/keepsorted/block.go index 060289b..ed51b34 100644 --- a/keepsorted/block.go +++ b/keepsorted/block.go @@ -390,7 +390,7 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) } } - if n := len(dataGroups); n > 1 && allEndInComma(dataGroups[0:n-1]) && !endInComma(dataGroups[n-1]) { + if n := len(dataGroups); n > 1 && allEndInComma(dataGroups[0:n-1]) && !dataGroups[n-1].matchesSuffix(commaLineEnd) { if dataGroups[n-1].matchesSuffix(commentLineEnd) { dataGroups[n-1].replaceSuffix(commentLineEnd, ", $2") } else { @@ -416,7 +416,7 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) func allEndInComma(lgs []*lineGroup) bool { for _, lg := range lgs { - if !endInComma(lg) { + if !lg.matchesSuffix(commaLineEnd) { return false } } @@ -427,7 +427,3 @@ var ( commaLineEnd = regexp.MustCompile("(,)( *)((#|//).*)?$") commentLineEnd = regexp.MustCompile("( *)((#|//).*)$") ) - -func endInComma(lg *lineGroup) bool { - return lg.matchesSuffix(commaLineEnd) -} From a85ec57d994569a2efed5d2d66ac641b8f35c217 Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 12:12:43 -0700 Subject: [PATCH 4/7] Remove spurious IDE changes Undo changes to comments made by renaming via the IDE. --- keepsorted/line_group.go | 2 +- keepsorted/options.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/keepsorted/line_group.go b/keepsorted/line_group.go index ca26cf9..0cb6adb 100644 --- a/keepsorted/line_group.go +++ b/keepsorted/line_group.go @@ -61,7 +61,7 @@ type accessRecorder struct { joinedComment bool } -// matchesAnyRegex returns true if s matchesSuffix one of the regexes. +// matchesAnyRegex returns true if s matches one of the regexes. func matchesAnyRegex(s string, regexes []*regexp.Regexp) bool { for _, regex := range regexes { if regex.FindStringSubmatch(s) != nil { diff --git a/keepsorted/options.go b/keepsorted/options.go index 45b84a5..6b5c6fa 100644 --- a/keepsorted/options.go +++ b/keepsorted/options.go @@ -453,7 +453,7 @@ func (opts blockOptions) hasGroupPrefix(s string) bool { } // trimIgnorePrefix removes the first matching IgnorePrefixes from s, if s -// matchesSuffix one of the IgnorePrefixes. +// matches one of the IgnorePrefixes. func (opts blockOptions) trimIgnorePrefix(s string) string { _, s, _ = opts.cutFirstPrefix(s, slices.Values(opts.IgnorePrefixes)) return s From 9cb6680314d0dd71340d525afd88c00f577f1989 Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 12:36:28 -0700 Subject: [PATCH 5/7] Add example badly-handled comments --- goldens/trailing_commas.in | 10 +++++++--- goldens/trailing_commas.out | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/goldens/trailing_commas.in b/goldens/trailing_commas.in index ff51589..f4645b8 100644 --- a/goldens/trailing_commas.in +++ b/goldens/trailing_commas.in @@ -7,7 +7,6 @@ Trailing comma except for last Trailing comma and a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 3, # three 1, @@ -15,7 +14,6 @@ TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test end Trailing comma except for last, last has a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 3, 1, @@ -23,9 +21,15 @@ TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test end Trailing comma and a comment, last has a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 3, # three 1, 2 # two keep-sorted-test end + +Trailing commas with quoted comment characters + keep-sorted-test start + "c", + "b, //", + "a, #" + keep-sorted-test end diff --git a/goldens/trailing_commas.out b/goldens/trailing_commas.out index f73bf12..e33b643 100644 --- a/goldens/trailing_commas.out +++ b/goldens/trailing_commas.out @@ -7,7 +7,6 @@ Trailing comma except for last Trailing comma and a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, 2, @@ -15,7 +14,6 @@ TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test end Trailing comma except for last, last has a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, 2, # two @@ -23,9 +21,15 @@ TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test end Trailing comma and a comment, last has a comment -TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start 1, 2, # two 3 # three keep-sorted-test end + +Trailing commas with quoted comment characters + keep-sorted-test start + "a, #" + "b, //", + "c", + keep-sorted-test end From 9c19ba4353bf73aca73b68b84455c9d2a616b836 Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 14:22:16 -0700 Subject: [PATCH 6/7] Remove globals Remove global regexp variables and move them into the function directly. Also change to "allMatchSuffix" to be closer to original code. --- keepsorted/block.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/keepsorted/block.go b/keepsorted/block.go index ed51b34..d385c69 100644 --- a/keepsorted/block.go +++ b/keepsorted/block.go @@ -390,7 +390,10 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) } } - if n := len(dataGroups); n > 1 && allEndInComma(dataGroups[0:n-1]) && !dataGroups[n-1].matchesSuffix(commaLineEnd) { + commaLineEnd := regexp.MustCompile("(,)( *)((#|//).*)?$") + commentLineEnd := regexp.MustCompile("( *)((#|//).*)$") + + if n := len(dataGroups); n > 1 && allMatchSuffix(dataGroups[0:n-1], commaLineEnd) && !dataGroups[n-1].matchesSuffix(commaLineEnd) { if dataGroups[n-1].matchesSuffix(commentLineEnd) { dataGroups[n-1].replaceSuffix(commentLineEnd, ", $2") } else { @@ -414,16 +417,11 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) return func([]*lineGroup) {} } -func allEndInComma(lgs []*lineGroup) bool { +func allMatchSuffix(lgs []*lineGroup, suffix *regexp.Regexp) bool { for _, lg := range lgs { - if !lg.matchesSuffix(commaLineEnd) { + if !lg.matchesSuffix(suffix) { return false } } return true } - -var ( - commaLineEnd = regexp.MustCompile("(,)( *)((#|//).*)?$") - commentLineEnd = regexp.MustCompile("( *)((#|//).*)$") -) From ab7d55566d4c76befb5b0784b0423e65e1d4d766 Mon Sep 17 00:00:00 2001 From: Will Beason Date: Thu, 19 Mar 2026 15:07:14 -0700 Subject: [PATCH 7/7] Simplify implementation and document edge cases Simplify implementation to not worry about spaces consistency. Instead, just preserve spaces before comments as-is. Also add examples that show when comments still aren't handled consistently. --- goldens/trailing_commas.in | 16 ++++++++++++---- goldens/trailing_commas.out | 18 +++++++++++++----- keepsorted/block.go | 23 +++++++++++------------ keepsorted/keep_sorted_test.go | 15 +++++++++++++++ keepsorted/options.go | 6 +++++- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/goldens/trailing_commas.in b/goldens/trailing_commas.in index f4645b8..00dee8a 100644 --- a/goldens/trailing_commas.in +++ b/goldens/trailing_commas.in @@ -16,18 +16,26 @@ Trailing comma and a comment Trailing comma except for last, last has a comment keep-sorted-test start 3, - 1, - 2 # two + 1, + 2 keep-sorted-test end Trailing comma and a comment, last has a comment keep-sorted-test start - 3, # three + 3, -- three 1, - 2 # two + 2 -- two + keep-sorted-test end + +Trailing commas with inline comment + keep-sorted-test start + "c" /* c */, + "b", + "a" /* a */ keep-sorted-test end Trailing commas with quoted comment characters +TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start "c", "b, //", diff --git a/goldens/trailing_commas.out b/goldens/trailing_commas.out index e33b643..6db1901 100644 --- a/goldens/trailing_commas.out +++ b/goldens/trailing_commas.out @@ -10,24 +10,32 @@ Trailing comma and a comment keep-sorted-test start 1, 2, - 3 # three + 3 # three keep-sorted-test end Trailing comma except for last, last has a comment keep-sorted-test start - 1, - 2, # two + 1, + 2, 3 keep-sorted-test end Trailing comma and a comment, last has a comment keep-sorted-test start 1, - 2, # two - 3 # three + 2, -- two + 3 -- three + keep-sorted-test end + +Trailing commas with inline comment + keep-sorted-test start + "a", /* a */ + "b", + "c" /* c */ keep-sorted-test end Trailing commas with quoted comment characters +TODO: https://github.com/google/keep-sorted/issues/33 - Fix this keep-sorted-test start "a, #" "b, //", diff --git a/keepsorted/block.go b/keepsorted/block.go index d385c69..730fb6b 100644 --- a/keepsorted/block.go +++ b/keepsorted/block.go @@ -390,24 +390,23 @@ func handleTrailingComma(lgs []*lineGroup) (trimTrailingComma func([]*lineGroup) } } - commaLineEnd := regexp.MustCompile("(,)( *)((#|//).*)?$") - commentLineEnd := regexp.MustCompile("( *)((#|//).*)$") + knownCommentMarkersList := knownCommentMarkers() + for i, marker := range knownCommentMarkersList { + // Some comment markers, such as "/*", include regex metacharacters. + knownCommentMarkersList[i] = regexp.QuoteMeta(marker) + } + knownCommentMarkersStr := strings.Join(knownCommentMarkersList, "|") + + commaLineEnd := regexp.MustCompile(fmt.Sprintf(",(\\s*(%s).*)?$", knownCommentMarkersStr)) + lineEnd := regexp.MustCompile(fmt.Sprintf("(\\s*(%s).*)?$", knownCommentMarkersStr)) if n := len(dataGroups); n > 1 && allMatchSuffix(dataGroups[0:n-1], commaLineEnd) && !dataGroups[n-1].matchesSuffix(commaLineEnd) { - if dataGroups[n-1].matchesSuffix(commentLineEnd) { - dataGroups[n-1].replaceSuffix(commentLineEnd, ", $2") - } else { - dataGroups[n-1].append(",") - } + dataGroups[n-1].replaceSuffix(lineEnd, ",$1") return func(lgs []*lineGroup) { for i := len(lgs) - 1; i >= 0; i-- { if len(lgs[i].lines) > 0 { - if lgs[i].matchesSuffix(commentLineEnd) { - lgs[i].replaceSuffix(commaLineEnd, " $3") - } else { - lgs[i].trimSuffix(",") - } + lgs[i].replaceSuffix(commaLineEnd, "$1") return } } diff --git a/keepsorted/keep_sorted_test.go b/keepsorted/keep_sorted_test.go index b90cd0b..1dbf752 100644 --- a/keepsorted/keep_sorted_test.go +++ b/keepsorted/keep_sorted_test.go @@ -1126,6 +1126,21 @@ func TestLineSorting(t *testing.T) { "foo", }, }, + { + name: "TrailingCommas_WithComments", + + in: []string{ + "foo,", + "baz, /* baz */", + "bar // bar", + }, + + want: []string{ + "bar, // bar", + "baz, /* baz */", + "foo", + }, + }, { name: "IgnorePrefixes", diff --git a/keepsorted/options.go b/keepsorted/options.go index 6b5c6fa..928a79f 100644 --- a/keepsorted/options.go +++ b/keepsorted/options.go @@ -312,9 +312,13 @@ func formatIntList(intVals []int) string { return strings.Join(vals, ",") } +func knownCommentMarkers() []string { + return []string{"//", "#", "/*", "--", ";", "