Skip to content

Commit b3b87ff

Browse files
authored
fmt: allow one liner rule grouping (open-policy-agent#7453)
While the double newline added by the formatter after each rule makes sense for most rules, short one-liner rules should be groupable. This PR changes the behavior of the formatter, so that if the user does: ```rego x := 1 y := 2 ``` That is no longer formatted into: ```rego x := 1 y := 2 ``` If the user **wants** double newlines between one-liner rules, the formatter respects those when present. Note that the `default` rules are excepted even when a one-liner, as presenting these separately helps understanding the policy. Fixes open-policy-agent#6760 Do note that the issued mentioned doing this only for incremental rules, i.e. to group only rules of the same name. I changed my mind on that though, as grouping "constants" should be possible too. Signed-off-by: Anders Eknert <[email protected]>
1 parent 92ae9a0 commit b3b87ff

9 files changed

+155
-23
lines changed

Diff for: v1/format/format.go

+26-4
Original file line numberDiff line numberDiff line change
@@ -437,16 +437,38 @@ func (w *writer) writeComments(comments []*ast.Comment) {
437437
}
438438

439439
func (w *writer) writeRules(rules []*ast.Rule, comments []*ast.Comment) []*ast.Comment {
440-
for _, rule := range rules {
440+
for i, rule := range rules {
441441
comments = w.insertComments(comments, rule.Location)
442442
comments = w.writeRule(rule, false, comments)
443+
444+
if i < len(rules)-1 && w.groupableOneLiner(rule) {
445+
next := rules[i+1]
446+
if w.groupableOneLiner(next) && next.Location.Row == rule.Location.Row+1 {
447+
// Current rule and the next are both groupable one-liners, and
448+
// adjacent in the original policy (i.e. no extra newlines between them).
449+
continue
450+
}
451+
}
452+
443453
w.blankLine()
444454
}
445455
return comments
446456
}
447457

448458
var expandedConst = ast.NewBody(ast.NewExpr(ast.InternedBooleanTerm(true)))
449459

460+
func (w *writer) groupableOneLiner(rule *ast.Rule) bool {
461+
// Location required to determine if two rules are adjacent in the policy.
462+
// If not, we respect line breaks between rules.
463+
if len(rule.Body) > 1 || rule.Default || rule.Location == nil {
464+
return false
465+
}
466+
467+
partialSetException := w.fmtOpts.contains || rule.Head.Value != nil
468+
469+
return (w.fmtOpts.regoV1 || w.fmtOpts.ifs) && partialSetException
470+
}
471+
450472
func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment) []*ast.Comment {
451473
if rule == nil {
452474
return comments
@@ -468,14 +490,14 @@ func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment)
468490

469491
comments = w.writeHead(rule.Head, rule.Default, isExpandedConst, comments)
470492

471-
// this excludes partial sets UNLESS `contains` is used
472-
partialSetException := w.fmtOpts.contains || rule.Head.Value != nil
473-
474493
if len(rule.Body) == 0 || isExpandedConst {
475494
w.endLine()
476495
return comments
477496
}
478497

498+
// this excludes partial sets UNLESS `contains` is used
499+
partialSetException := w.fmtOpts.contains || rule.Head.Value != nil
500+
479501
if (w.fmtOpts.regoV1 || w.fmtOpts.ifs) && partialSetException {
480502
w.write(" if")
481503
if len(rule.Body) == 1 {

Diff for: v1/format/format_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,34 @@ p contains x if {
907907
}
908908
}
909909

910+
func TestGroupableOneLinerRules(t *testing.T) {
911+
contents := []byte(`package test
912+
913+
foo := 1 if input.x
914+
foo := 2 if not input.x
915+
916+
a := 1
917+
b := 2
918+
919+
c := 3
920+
921+
d := 4
922+
923+
# comment above group
924+
e := 5
925+
f := 6
926+
`)
927+
928+
formatted, err := Source("test.rego", contents)
929+
if err != nil {
930+
t.Fatalf("unexpected error: %v", err)
931+
}
932+
933+
if !bytes.Equal(formatted, contents) {
934+
t.Fatalf("expected %q but got %q", formatted, contents)
935+
}
936+
}
937+
910938
// 382 3064960 ns/op 4573131 B/op 26266 allocs/op // no optimizations
911939
// 685 1737719 ns/op 1972193 B/op 14160 allocs/op // pre-allocate partitionComments
912940
// 708 1674343 ns/op 1916700 B/op 11556 allocs/op // static memberRef & memberWithKeyRef

Diff for: v1/format/testfiles/v0/test_ref_heads.rego.formatted

-7
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,13 @@ package test
33
import future.keywords
44

55
a.b.c = "d"
6-
76
a.b.e = "f"
8-
97
a.b.g contains x if some x in numbers.range(1, 3)
10-
118
a.b.h[x] = 1 if x := "one"
129

1310
q[1] = y
14-
1511
r[x] if x := 10
16-
1712
p.q.r[x] if x := 10
18-
1913
p.q.r[2] = true
2014

2115
g[h].i[j].k = true
@@ -31,7 +25,6 @@ g[3].i[j].k = x if {
3125
}
3226

3327
g[h].i[j].k[l] = true
34-
3528
g[h].i[j].k[l] contains x if x = "foo"
3629

3730
g[h].i[j].k[l] contains x if {

Diff for: v1/format/testfiles/v1/test.rego.formatted

-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ globals := {
2525
}
2626

2727
partial_obj["x"] := 1
28-
2928
partial_obj["y"] := 2
3029

3130
partial_obj["z"] := 3
@@ -79,7 +78,6 @@ short(x) if {
7978
}
8079

8180
raw_string := `hi\there`
82-
8381
raw_multiline := `this
8482
string
8583
is on
@@ -214,7 +212,6 @@ declare1 := 1
214212
declare2 := 2 if false
215213

216214
declare3 := {1, 2, 3}
217-
218215
declare4 := {4, 5, 6}
219216

220217
union_object := {"response": (declare3 | declare4)}

Diff for: v1/format/testfiles/v1/test_grouping.rego

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package p
2+
3+
x := 1
4+
y := 2
5+
6+
x1 := 1
7+
8+
x2 := 2
9+
10+
a.b.c := 1
11+
a.b.d := 2
12+
13+
s contains 1
14+
s contains 2
15+
16+
s contains 3
17+
18+
s contains 4
19+
20+
rule if foo == bar
21+
rule if bar == foo
22+
23+
long if {
24+
x := 1
25+
y := 2
26+
}
27+
long if {
28+
x := 2
29+
y := 3
30+
}
31+
32+
short if condition
33+
not_short if {
34+
rule
35+
body
36+
}
37+
38+
not_short if {
39+
rule
40+
body
41+
}
42+
short if condition
43+
44+
s contains "foo"
45+
s contains "bar" if {
46+
foo
47+
bar
48+
}
49+

Diff for: v1/format/testfiles/v1/test_grouping.rego.formatted

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package p
2+
3+
x := 1
4+
y := 2
5+
6+
x1 := 1
7+
8+
x2 := 2
9+
10+
a.b.c := 1
11+
a.b.d := 2
12+
13+
s contains 1
14+
s contains 2
15+
16+
s contains 3
17+
18+
s contains 4
19+
20+
rule if foo == bar
21+
rule if bar == foo
22+
23+
long if {
24+
x := 1
25+
y := 2
26+
}
27+
28+
long if {
29+
x := 2
30+
y := 3
31+
}
32+
33+
short if condition
34+
35+
not_short if {
36+
rule
37+
body
38+
}
39+
40+
not_short if {
41+
rule
42+
body
43+
}
44+
45+
short if condition
46+
47+
s contains "foo"
48+
49+
s contains "bar" if {
50+
foo
51+
bar
52+
}

Diff for: v1/format/testfiles/v1/test_in_operator_with_parenthesis.rego.formatted

-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,4 @@ z if {
1414
}
1515

1616
f(_, _) := true
17-
1817
g(_) := true

Diff for: v1/format/testfiles/v1/test_issue_5537_with_ref.rego.formatted

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package p
22

33
first := {"one", "two"}
4-
54
second := {"two", "three"}
65

76
example contains msg if {

Diff for: v1/format/testfiles/v1/test_ref_heads.rego.formatted

-7
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
package test
22

33
a.b.c := "d"
4-
54
a.b.e := "f"
6-
75
a.b.g contains x if some x in numbers.range(1, 3)
8-
96
a.b.h[x] := 1 if x := "one"
107

118
q[1] := y
12-
139
r[x] if x := 10
14-
1510
p.q.r[x] if x := 10
16-
1711
p.q.r[2] := true
1812

1913
g[h].i[j].k := true
@@ -29,7 +23,6 @@ g[3].i[j].k := x if {
2923
}
3024

3125
g[h].i[j].k[l] := true
32-
3326
g[h].i[j].k[l] contains x if x = "foo"
3427

3528
g[h].i[j].k[l] contains x if {

0 commit comments

Comments
 (0)