Skip to content

Commit 4c4bb29

Browse files
willbeasonJeffFaer
andauthored
feat: Make skip_lines option allow skipping at end (#122)
This adds feature discussed in #80 skip_lines now interprets negative values as lines to skip at the end of a sorted region. skip_lines now accepts up to two values. If two values are specified, the first must be lines skipped at the start and the second lines skipped at the end. Internally this is represented as an int slice. Add ability for parse options to handle int slices. Modify golden examples to maintain intended behavior. Update documentation with example of skipping lines both at the start and end. --------- Co-authored-by: Jeffrey Faer <jeffrey.faer@gmail.com>
1 parent b78ccf3 commit 4c4bb29

File tree

10 files changed

+194
-16
lines changed

10 files changed

+194
-16
lines changed

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,11 @@ treated as sticky. These prefixes cannot contain space characters.
396396
#### Skipping lines
397397

398398
In some cases, it may not be possible to have the start directive on the line
399-
immediately before the sorted region. In this case, `skip_lines` can be used to
400-
indicate how many lines are to be skipped before the sorted region.
399+
immediately before the sorted region or end immediately after. In these cases,
400+
`skip_lines` can be used to indicate how many lines are to be skipped before
401+
and/or after the sorted region. The `skip_lines` option interprets positive
402+
values as lines to skip at the start of the sorted region and negative values
403+
as lines to skip at the end.
401404

402405
For instance, this can be used with a Markdown table, to prevent the headers
403406
and the dashed line after the headers from being sorted:
@@ -435,6 +438,43 @@ Alpha | Foo
435438
</tr>
436439
</table>
437440

441+
This can be used to keep the footers of a table from being sorted as well:
442+
443+
<table>
444+
<tr>
445+
<td>
446+
447+
```md
448+
449+
Item | Cost
450+
----- | -----
451+
Lemon | $1.50
452+
Pear | $2.10
453+
Apple | $1.09
454+
----- | -----
455+
Total | $4.69
456+
457+
```
458+
459+
</td>
460+
<td>
461+
462+
```diff
463+
+<!-- keep-sorted start skip_lines=2,-2 -->
464+
Item | Cost
465+
----- | -----
466+
Apple | $1.09
467+
Lemon | $1.50
468+
Pear | $2.10
469+
----- | -----
470+
Total | $4.69
471+
+<!-- keep-sorted end -->
472+
```
473+
474+
</td>
475+
</tr>
476+
</table>
477+
438478
### Sorting options
439479

440480
Sorting options tell keep-sorted how the logical lines in your keep-sorted

goldens/simple.err

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
WRN skip_lines has invalid value: -1 line=113
1+
WRN skip_lines has conflicting values (should one of these be positive, to skip lines at the start of the block instead?): -1,-1 line=113
22
WRN unrecognized option "foo" line=113
33
WRN while parsing option "ignore_prefixes": content appears to be an unterminated YAML list: "[abc, foo" line=120
44
exit status 1

goldens/simple.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ B
110110
// keep-sorted-test end
111111

112112
Invalid option
113-
keep-sorted-test start group=yes skip_lines=-1 foo=bar
113+
keep-sorted-test start group=yes skip_lines=-1,-1 foo=bar
114114
2
115115
1
116116
3

goldens/simple.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ C
111111
// keep-sorted-test end
112112

113113
Invalid option
114-
keep-sorted-test start group=yes skip_lines=-1 foo=bar
114+
keep-sorted-test start group=yes skip_lines=-1,-1 foo=bar
115115
1
116116
2
117117
3

keepsorted/block.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ func (f *Fixer) newBlocks(filename string, lines []string, offset int, include f
113113
warnings = append(warnings, finding(filename, start.index+offset, start.index+offset, warn.Error()))
114114
}
115115

116-
start.index += opts.SkipLines
116+
start.index += opts.startOffset()
117+
endIndex += opts.endOffset()
117118
if start.index >= endIndex {
118119
continue
119120
}

keepsorted/keep_sorted_test.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func TestFindings(t *testing.T) {
222222
want: []*Finding{finding(filename, 3, 5, errorUnordered, automaticReplacement(3, 5, "1\n2\n3\n"))},
223223
},
224224
{
225-
name: "SkipLines",
225+
name: "SkipLinesStart",
226226

227227
in: `
228228
// keep-sorted-test start skip_lines=2
@@ -235,6 +235,48 @@ func TestFindings(t *testing.T) {
235235

236236
want: []*Finding{finding(filename, 5, 7, errorUnordered, automaticReplacement(5, 7, "1\n2\n3\n"))},
237237
},
238+
{
239+
name: "SkipLinesEnd",
240+
241+
in: `
242+
// keep-sorted-test start skip_lines=-2
243+
5
244+
4
245+
3
246+
2
247+
1
248+
// keep-sorted-test end`,
249+
250+
want: []*Finding{finding(filename, 3, 5, errorUnordered, automaticReplacement(3, 5, "3\n4\n5\n"))},
251+
},
252+
{
253+
name: "SkipLinesStartAndEnd",
254+
255+
in: `
256+
// keep-sorted-test start skip_lines=1,-1
257+
5
258+
4
259+
3
260+
2
261+
1
262+
// keep-sorted-test end`,
263+
264+
want: []*Finding{finding(filename, 4, 6, errorUnordered, automaticReplacement(4, 6, "2\n3\n4\n"))},
265+
},
266+
{
267+
name: "SkipLinesEndAndStart",
268+
269+
in: `
270+
// keep-sorted-test start skip_lines=-1,1
271+
5
272+
4
273+
3
274+
2
275+
1
276+
// keep-sorted-test end`,
277+
278+
want: []*Finding{finding(filename, 4, 6, errorUnordered, automaticReplacement(4, 6, "2\n3\n4\n"))},
279+
},
238280
{
239281
name: "MismatchedStart",
240282

keepsorted/options.go

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func (opts BlockOptions) String() string {
7575
// - []string: key=a,b,c,d
7676
// - map[string]bool: key=a,b,c,d
7777
// - int: key=123
78+
// - []int: key=1,-1
7879
// - ByRegexOptions key=a,b,c,d, key=[yaml_list]
7980
type blockOptions struct {
8081
// AllowYAMLLists determines whether list.set valued options are allowed to be specified by YAML.
@@ -85,7 +86,7 @@ type blockOptions struct {
8586
///////////////////////////
8687

8788
// SkipLines is the number of lines to ignore before sorting.
88-
SkipLines int `key:"skip_lines"`
89+
SkipLines []int `key:"skip_lines"`
8990
// Group determines whether we group lines together based on increasing indentation.
9091
Group bool
9192
// GroupPrefixes tells us about other types of lines that should be added to a group.
@@ -243,6 +244,8 @@ func formatValue(val reflect.Value) (string, error) {
243244
}
244245
case reflect.TypeFor[int]():
245246
return strconv.Itoa(int(val.Int())), nil
247+
case reflect.TypeFor[[]int]():
248+
return formatIntList(val.Interface().([]int)), nil
246249
case reflect.TypeFor[[]ByRegexOption]():
247250
opts := val.Interface().([]ByRegexOption)
248251
vals := make([]string, 0, len(opts))
@@ -301,6 +304,14 @@ func formatList(vals []string) (string, error) {
301304
return strings.TrimSpace(string(out)), nil
302305
}
303306

307+
func formatIntList(intVals []int) string {
308+
vals := make([]string, 0, len(intVals))
309+
for _, v := range intVals {
310+
vals = append(vals, strconv.Itoa(v))
311+
}
312+
return strings.Join(vals, ",")
313+
}
314+
304315
func guessCommentMarker(startLine string) string {
305316
startLine = strings.TrimSpace(startLine)
306317
for _, marker := range []string{"//", "#", "/*", "--", ";", "<!--"} {
@@ -323,9 +334,22 @@ func (opts *blockOptions) setCommentMarker(marker string) {
323334

324335
func validate(opts *blockOptions) (warnings []error) {
325336
var warns []error
326-
if opts.SkipLines < 0 {
327-
warns = append(warns, fmt.Errorf("skip_lines has invalid value: %v", opts.SkipLines))
328-
opts.SkipLines = 0
337+
if len(opts.SkipLines) > 2 {
338+
warns = append(warns, fmt.Errorf("skip_lines accepts at most two values: %v", formatIntList(opts.SkipLines)))
339+
opts.SkipLines = nil
340+
} else if len(opts.SkipLines) == 2 {
341+
if cmp.Compare(opts.SkipLines[0], 0) == cmp.Compare(opts.SkipLines[1], 0) {
342+
// Both are the same sign. It's okay for both to be 0.
343+
if opts.SkipLines[0] < 0 {
344+
// Both are negative.
345+
warns = append(warns, fmt.Errorf("skip_lines has conflicting values (should one of these be positive, to skip lines at the start of the block instead?): %v", formatIntList(opts.SkipLines)))
346+
opts.SkipLines = nil
347+
} else if opts.SkipLines[0] > 0 {
348+
// Both are positive.
349+
warns = append(warns, fmt.Errorf("skip_lines has conflicting values (should one of these be negative, to skip lines at the end of the block instead?): %v", formatIntList(opts.SkipLines)))
350+
opts.SkipLines = nil
351+
}
352+
}
329353
}
330354

331355
if opts.NewlineSeparated < 0 {
@@ -531,6 +555,28 @@ func (opts blockOptions) maybeParseNumeric(s string) numericTokens {
531555
return t
532556
}
533557

558+
func (opts blockOptions) startOffset() int {
559+
if len(opts.SkipLines) == 2 {
560+
return max(opts.SkipLines[0], opts.SkipLines[1])
561+
} else if len(opts.SkipLines) == 1 {
562+
if opts.SkipLines[0] > 0 {
563+
return opts.SkipLines[0]
564+
}
565+
}
566+
return 0
567+
}
568+
569+
func (opts blockOptions) endOffset() int {
570+
if len(opts.SkipLines) == 2 {
571+
return min(opts.SkipLines[0], opts.SkipLines[1])
572+
} else if len(opts.SkipLines) == 1 {
573+
if opts.SkipLines[0] < 0 {
574+
return opts.SkipLines[0]
575+
}
576+
}
577+
return 0
578+
}
579+
534580
// numericTokens is the result of parsing all numeric tokens out of a string.
535581
//
536582
// e.g. a string like "Foo_123" becomes

keepsorted/options_parser.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ func (p *parser) popValue(typ reflect.Type) (reflect.Value, error) {
5959
case reflect.TypeFor[int]():
6060
val, err := p.popInt()
6161
return reflect.ValueOf(val), err
62+
case reflect.TypeFor[[]int]():
63+
val, err := p.popIntList()
64+
return reflect.ValueOf(val), err
6265
case reflect.TypeFor[SortOrder]():
6366
val, err := p.popSortOrder()
6467
return reflect.ValueOf(val), err
@@ -189,6 +192,10 @@ func (p *parser) popList() ([]string, error) {
189192
return popListValue(p, func(s string) (string, error) { return s, nil })
190193
}
191194

195+
func (p *parser) popIntList() ([]int, error) {
196+
return popListValue(p, strconv.Atoi)
197+
}
198+
192199
func (p *parser) popByRegexOption() ([]ByRegexOption, error) {
193200
return popListValue(p, func(s string) (ByRegexOption, error) {
194201
pat, err := regexp.Compile(s)

keepsorted/options_parser_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,24 @@ func TestPopValue(t *testing.T) {
5252
want: int(0),
5353
wantErr: true,
5454
},
55+
{
56+
name: "IntList_Empty",
57+
58+
input: "",
59+
want: []int{},
60+
},
61+
{
62+
name: "IntList_SingleElement",
63+
64+
input: "1",
65+
want: []int{1},
66+
},
67+
{
68+
name: "IntList_MultipleElements_WithRepeats",
69+
70+
input: "1,-2,1",
71+
want: []int{1, -2, 1},
72+
},
5573
{
5674
name: "List_Empty",
5775

keepsorted/options_test.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,22 @@ func TestBlockOptions(t *testing.T) {
7070
want: blockOptions{Group: true},
7171
},
7272
{
73-
name: "SkipLines",
73+
name: "SkipLinesStart",
7474
in: "skip_lines=10",
7575

76-
want: blockOptions{SkipLines: 10},
76+
want: blockOptions{SkipLines: []int{10}},
77+
},
78+
{
79+
name: "SkipLinesEnd",
80+
in: "skip_lines=-3",
81+
82+
want: blockOptions{SkipLines: []int{-3}},
83+
},
84+
{
85+
name: "SkipLinesStartAndEnd",
86+
in: "skip_lines=2,-1",
87+
88+
want: blockOptions{SkipLines: []int{2, -1}},
7789
},
7890
{
7991
name: "NewlineSeparated_Bool",
@@ -94,10 +106,22 @@ func TestBlockOptions(t *testing.T) {
94106
wantErr: "newline_separated has invalid value: -1",
95107
},
96108
{
97-
name: "ErrorSkipLinesIsNegative",
98-
in: "skip_lines=-1",
109+
name: "ErrorSkipBothNegative",
110+
in: "skip_lines=-1,-1",
111+
112+
wantErr: "skip_lines has conflicting values (should one of these be positive, to skip lines at the start of the block instead?): -1,-1",
113+
},
114+
{
115+
name: "ErrorSkipBothPositive",
116+
in: "skip_lines=1,1",
117+
118+
wantErr: "skip_lines has conflicting values (should one of these be negative, to skip lines at the end of the block instead?): 1,1",
119+
},
120+
{
121+
name: "ErrorSkipLinesTooMany",
122+
in: "skip_lines=1,-1,2",
99123

100-
wantErr: "skip_lines has invalid value: -1",
124+
wantErr: "skip_lines accepts at most two values: 1,-1,2",
101125
},
102126
{
103127
name: "ItemList",

0 commit comments

Comments
 (0)