Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions goldens/group_delimiter_regexes.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Sort by consecutive newlines:
// keep-sorted-test start group_delimiter_regexes=['^$']
b-block
attached line 1
attached line 2

a-block
attached line 3
attached line 4
// keep-sorted-test end

Sort by semicolon:
// keep-sorted-test start group_delimiter_regexes=[';$']
XXX a semicolon ; in the middle is ignored
But at the end it terminates the group ;

ZZZ This group will come after YYY;

YYY will be sorted up;
// keep-sorted-test end
20 changes: 20 additions & 0 deletions goldens/group_delimiter_regexes.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Sort by consecutive newlines:
// keep-sorted-test start group_delimiter_regexes=['^$']
a-block
attached line 3
attached line 4
b-block
attached line 1
attached line 2

// keep-sorted-test end

Sort by semicolon:
// keep-sorted-test start group_delimiter_regexes=[';$']
XXX a semicolon ; in the middle is ignored
But at the end it terminates the group ;

YYY will be sorted up;

ZZZ This group will come after YYY;
// keep-sorted-test end
47 changes: 47 additions & 0 deletions keepsorted/keep_sorted_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package keepsorted

import (
"regexp"
"strings"
"testing"

Expand Down Expand Up @@ -1632,6 +1633,52 @@ func TestLineGrouping(t *testing.T) {
`"""`}},
},
},
{
name: "GroupDelimiter_BlankLine",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding a golden test or two for this new option?

opts: blockOptions{
GroupDelimiterRegexes: []RegexOption{
{Pattern: regexp.MustCompile(`^$`)},
},
},

want: []lineGroupContent{
{lines: []string{
"[toml]",
"key=value",
"",
}},
{lines: []string{
"[block_two]",
"key=value",
"",
}},
{lines: []string{
"[block_three]",
"final_key=value",
}},
},
},
{
name: "GroupDelimiter_Semicolon",
opts: blockOptions{
GroupDelimiterRegexes: []RegexOption{
{Pattern: regexp.MustCompile(`;$`)},
},
},

want: []lineGroupContent{
{lines: []string{
"statement ; in middle",
"semicolon at end;",
}},
{lines: []string{
"Next paragraph;",
}},
{lines: []string{
"And the final one",
}},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
initZerolog(t)
Expand Down
13 changes: 12 additions & 1 deletion keepsorted/line_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ func groupLines(lines []string, metadata blockMetadata) []*lineGroup {
} else {
commentRange.append(i)
}
} else if len(metadata.opts.GroupDelimiterRegexes) != 0 {
appendLine(i, l)
for _, match := range metadata.opts.matchRegexes(l, metadata.opts.GroupDelimiterRegexes) {
if match == nil {
continue
}
if !lineRange.empty() {
finishGroup()
}
break
}
} else {
if !lineRange.empty() {
finishGroup()
Expand Down Expand Up @@ -361,7 +372,7 @@ func (lg *lineGroup) commentOnly() bool {

func (lg *lineGroup) regexTokens() []regexToken {
// TODO: jfaer - Should we match regexes on the original content?
regexMatches := lg.opts.matchRegexes(lg.internalJoinedLines())
regexMatches := lg.opts.matchRegexes(lg.internalJoinedLines(), lg.opts.ByRegex)
ret := make([]regexToken, len(regexMatches))
if lg.access.regexTokens == nil {
lg.access.regexTokens = make([]regexTokenAccessRecorder, len(regexMatches))
Expand Down
23 changes: 13 additions & 10 deletions keepsorted/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
// true is unmarshaled as 1, false as 0.
type IntOrBool int

type ByRegexOption struct {
type RegexOption struct {
Pattern *regexp.Regexp
Template *string
}
Expand Down Expand Up @@ -67,7 +67,7 @@ func (opts BlockOptions) String() string {
// - []string: key=a,b,c,d
// - map[string]bool: key=a,b,c,d
// - int: key=123
// - ByRegexOptions key=a,b,c,d, key=[yaml_list]
// - []RegexOptions: key=a,b,c,d, key=[yaml_list]
type blockOptions struct {
// AllowYAMLLists determines whether list.set valued options are allowed to be specified by YAML.
AllowYAMLLists bool `key:"allow_yaml_lists"`
Expand All @@ -88,6 +88,8 @@ type blockOptions struct {
StickyComments bool `key:"sticky_comments"`
// StickyPrefixes tells us about other types of lines that should behave as sticky comments.
StickyPrefixes map[string]bool `key:"sticky_prefixes"`
// GroupDelimiterRegexes tells us if a line is allowed to end a group.
GroupDelimiterRegexes []RegexOption `key:"group_delimiter_regexes"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if someone provides a replacement template with this option? Is that even valid?

I wonder if it should actually be separate from the ByRegexpOption


///////////////////////
// Sorting options //
Expand All @@ -102,7 +104,7 @@ type blockOptions struct {
// IgnorePrefixes is a slice of prefixes that we do not consider when sorting lines.
IgnorePrefixes []string `key:"ignore_prefixes"`
// ByRegex is a slice of regexes that are used to extract the pieces of the line group that keep-sorted should sort by.
ByRegex []ByRegexOption `key:"by_regex"`
ByRegex []RegexOption `key:"by_regex"`

////////////////////////////
// Post-sorting options //
Expand Down Expand Up @@ -210,8 +212,8 @@ func formatValue(val reflect.Value) (string, error) {
return strconv.Itoa(int(val.Int())), nil
case reflect.TypeFor[int]():
return strconv.Itoa(int(val.Int())), nil
case reflect.TypeFor[[]ByRegexOption]():
opts := val.Interface().([]ByRegexOption)
case reflect.TypeFor[[]RegexOption]():
opts := val.Interface().([]RegexOption)
vals := make([]string, 0, len(opts))
seenTemplate := false
for _, opt := range opts {
Expand Down Expand Up @@ -390,20 +392,20 @@ func (opts blockOptions) trimIgnorePrefix(s string) string {
return s
}

// matchRegexes applies ByRegex to s.
// If ByRegex is empty, returns a slice that contains just s.
// matchRegexes applies regexes to s.
// If regexes is empty, returns a slice that contains just s.
// Otherwise, applies each regex to s in sequence:
// If a regex has capturing groups, the capturing groups will be added to the
// resulting slice.
// If a regex does not have capturing groups, all matched text will be added to
// the resulting slice.
func (opts blockOptions) matchRegexes(s string) []regexMatch {
if len(opts.ByRegex) == 0 {
func (opts blockOptions) matchRegexes(s string, regexes []RegexOption) []regexMatch {
if len(regexes) == 0 {
return []regexMatch{{s}}
}

var ret []regexMatch
for _, p := range opts.ByRegex {
for _, p := range regexes {
regex := p.Pattern

if p.Template != nil {
Expand All @@ -421,6 +423,7 @@ func (opts blockOptions) matchRegexes(s string) []regexMatch {
}

m := regex.FindStringSubmatch(s)

if m == nil {
ret = append(ret, regexDidNotMatch)
continue
Expand Down
10 changes: 5 additions & 5 deletions keepsorted/options_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (p *parser) popValue(typ reflect.Type) (reflect.Value, error) {
case reflect.TypeFor[map[string]bool]():
val, err := p.popSet()
return reflect.ValueOf(val), err
case reflect.TypeFor[[]ByRegexOption]():
case reflect.TypeFor[[]RegexOption]():
val, err := p.popListRegexOption()
if err != nil {
return reflect.Zero(typ), err
Expand Down Expand Up @@ -113,7 +113,7 @@ func (p *parser) popIntOrBool() (IntOrBool, error) {
return IntOrBool(i), nil
}

func (ar *ByRegexOption) UnmarshalYAML(node *yaml.Node) error {
func (ar *RegexOption) UnmarshalYAML(node *yaml.Node) error {
switch node.Tag {
case "!!str":
pat, err := regexp.Compile(node.Value)
Expand Down Expand Up @@ -180,10 +180,10 @@ func (p *parser) popList() ([]string, error) {
return popListValue(p, func(s string) (string, error) { return s, nil })
}

func (p *parser) popListRegexOption() ([]ByRegexOption, error) {
return popListValue(p, func(s string) (ByRegexOption, error) {
func (p *parser) popListRegexOption() ([]RegexOption, error) {
return popListValue(p, func(s string) (RegexOption, error) {
pat, err := regexp.Compile(s)
return ByRegexOption{Pattern: pat}, err
return RegexOption{Pattern: pat}, err
})
}

Expand Down
6 changes: 3 additions & 3 deletions keepsorted/options_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,14 @@ func TestPopValue(t *testing.T) {
name: "Regex",

input: ".*",
want: []ByRegexOption{{regexp.MustCompile(".*"), nil}},
want: []RegexOption{{regexp.MustCompile(".*"), nil}},
},
{
name: "MultipleRegex",

input: `[.*, abcd, '(?:efgh)ijkl']`,
allowYAMLList: true,
want: []ByRegexOption{
want: []RegexOption{
{regexp.MustCompile(".*"), nil},
{regexp.MustCompile("abcd"), nil},
{regexp.MustCompile("(?:efgh)ijkl"), nil},
Expand All @@ -236,7 +236,7 @@ func TestPopValue(t *testing.T) {

input: `[.*, Mon: 0, '\b(\d{2})/(\d{2})/(\d{4})\b': '${3}-${1}-${2}', "0: 1": 2]`,
allowYAMLList: true,
want: []ByRegexOption{
want: []RegexOption{
{regexp.MustCompile(".*"), nil},
{regexp.MustCompile("Mon"), &([]string{"0"})[0]},
{regexp.MustCompile(`\b(\d{2})/(\d{2})/(\d{4})\b`), &([]string{"${3}-${1}-${2}"})[0]},
Expand Down
21 changes: 17 additions & 4 deletions keepsorted/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func TestBlockOptions(t *testing.T) {

want: blockOptions{
AllowYAMLLists: true,
ByRegex: []ByRegexOption{
ByRegex: []RegexOption{
{regexp.MustCompile("(?:abcd)"), nil}, {regexp.MustCompile("efg.*"), nil},
},
},
Expand All @@ -205,13 +205,26 @@ func TestBlockOptions(t *testing.T) {

want: blockOptions{
AllowYAMLLists: true,
ByRegex: []ByRegexOption{
ByRegex: []RegexOption{
{Pattern: regexp.MustCompile(`.*`)},
{Pattern: regexp.MustCompile(`\b(\d{2})/(\d{2})/(\d{4})\b`),
Template: &[]string{"${3}-${1}-${2}"}[0]},
},
},
},
{
name: "GroupDelimiterRegexes",
in: `group_delimiter_regexes=['^$', ';$']`,
defaultOptions: blockOptions{AllowYAMLLists: true},

want: blockOptions{
AllowYAMLLists: true,
GroupDelimiterRegexes: []RegexOption{
{Pattern: regexp.MustCompile(`^$`)},
{Pattern: regexp.MustCompile(`;$`)},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
initZerolog(t)
Expand Down Expand Up @@ -325,10 +338,10 @@ func TestBlockOptions_regexTransform(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
var opts blockOptions
for _, regex := range tc.regexes {
opts.ByRegex = append(opts.ByRegex, ByRegexOption{regexp.MustCompile(regex), nil})
opts.ByRegex = append(opts.ByRegex, RegexOption{regexp.MustCompile(regex), nil})
}

gotTokens := opts.matchRegexes(tc.in)
gotTokens := opts.matchRegexes(tc.in, opts.ByRegex)
got := make([][]string, len(gotTokens))
for i, t := range gotTokens {
got[i] = []string(t)
Expand Down