Skip to content

Commit

Permalink
format: only use ref heads for all rule heads if necessary (#5450)
Browse files Browse the repository at this point in the history
Before, we'd end up formatting

    ps["foo"] = "bar" { true }

as

    ps.foo = "bar" { true }

and older OPA version know how to parse the former, but not
the latter.

Fixes #5449.

Also includes:
* format: pass internal options via struct; because adding a third (in some cases
   fifth) boolean argument just didn't seem right.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
  • Loading branch information
srenatus committed Dec 7, 2022
1 parent b7fb9d2 commit c0866e8
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 37 deletions.
89 changes: 56 additions & 33 deletions format/format.go
Expand Up @@ -61,6 +61,24 @@ func Ast(x interface{}) ([]byte, error) {
return AstWithOpts(x, Opts{})
}

type fmtOpts struct {
// When the future keyword "contains" is imported, all the pretty-printed
// modules will use that format for partial sets.
// NOTE(sr): For ref-head rules, this will be the default behaviour, since
// we need "contains" to disambiguate complete rules from partial sets.
contains bool

// Same logic applies as for "contains": if `future.keywords.if` (or all
// future keywords) is imported, we'll render rules that can use `if` with
// `if`.
ifs bool

// We check all rule ref heads to see if any of them _requires_ support
// for ref heads -- if they do, we'll print all of them in a different way
// than if they don't.
refHeads bool
}

func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
// The node has to be deep copied because it may be mutated below. Alternatively,
// we could avoid the copy by checking if mutation will occur first. For now,
Expand All @@ -75,16 +93,7 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
// present.
extraFutureKeywordImports := map[string]struct{}{}

// When the future keyword "contains" is imported, all the pretty-printed
// modules will use that format for partial sets.
// NOTE(sr): For ref-head rules, this will be the default behaviour, since
// we need "contains" to disambiguate complete rules from partial sets.
useContainsKW := false

// Same logic applies as for "contains": if `future.keywords.if` (or all
// future keywords) is imported, we'll render rules that can use `if` with
// `if`.
useIf := false
o := fmtOpts{}

// Preprocess the AST. Set any required defaults and calculate
// values required for printing the formatted output.
Expand All @@ -108,12 +117,20 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
case *ast.Import:
switch {
case future.IsAllFutureKeywords(n):
useContainsKW = true
useIf = true
o.contains = true
o.ifs = true
case future.IsFutureKeyword(n, "contains"):
useContainsKW = true
o.contains = true
case future.IsFutureKeyword(n, "if"):
useIf = true
o.ifs = true
}

case *ast.Rule:
if len(n.Head.Ref()) > 2 {
o.refHeads = true
}
if len(n.Head.Ref()) == 2 && n.Head.Key != nil && n.Head.Value == nil { // p.q contains "x"
o.refHeads = true
}
}

Expand All @@ -132,19 +149,18 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
for kw := range extraFutureKeywordImports {
x.Imports = ensureFutureKeywordImport(x.Imports, kw)
}
w.writeModule(x, useContainsKW, useIf)
w.writeModule(x, o)
case *ast.Package:
w.writePackage(x, nil)
case *ast.Import:
w.writeImports([]*ast.Import{x}, nil)
case *ast.Rule:
w.writeRule(x, false /* isElse */, useContainsKW, useIf, nil)
w.writeRule(x, false /* isElse */, o, nil)
case *ast.Head:
w.writeHead(x,
false, // isDefault
false, // isExpandedConst
useContainsKW,
useIf,
o,
nil)
case ast.Body:
w.writeBody(x, nil)
Expand Down Expand Up @@ -214,7 +230,7 @@ type writer struct {
delay bool
}

func (w *writer) writeModule(module *ast.Module, useContainsKW, useIf bool) {
func (w *writer) writeModule(module *ast.Module, o fmtOpts) {
var pkg *ast.Package
var others []interface{}
var comments []*ast.Comment
Expand Down Expand Up @@ -253,7 +269,7 @@ func (w *writer) writeModule(module *ast.Module, useContainsKW, useIf bool) {
imports, others = gatherImports(others)
comments = w.writeImports(imports, comments)
rules, others = gatherRules(others)
comments = w.writeRules(rules, useContainsKW, useIf, comments)
comments = w.writeRules(rules, o, comments)
}

for i, c := range comments {
Expand Down Expand Up @@ -283,16 +299,16 @@ func (w *writer) writeComments(comments []*ast.Comment) {
}
}

func (w *writer) writeRules(rules []*ast.Rule, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
func (w *writer) writeRules(rules []*ast.Rule, o fmtOpts, comments []*ast.Comment) []*ast.Comment {
for _, rule := range rules {
comments = w.insertComments(comments, rule.Location)
comments = w.writeRule(rule, false, useContainsKW, useIf, comments)
comments = w.writeRule(rule, false, o, comments)
w.blankLine()
}
return comments
}

func (w *writer) writeRule(rule *ast.Rule, isElse, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
func (w *writer) writeRule(rule *ast.Rule, isElse bool, o fmtOpts, comments []*ast.Comment) []*ast.Comment {
if rule == nil {
return comments
}
Expand All @@ -311,25 +327,25 @@ func (w *writer) writeRule(rule *ast.Rule, isElse, useContainsKW, useIf bool, co
// pretend that the rule has no body in this case.
isExpandedConst := rule.Body.Equal(ast.NewBody(ast.NewExpr(ast.BooleanTerm(true)))) && rule.Else == nil

comments = w.writeHead(rule.Head, rule.Default, isExpandedConst, useContainsKW, useIf, comments)
comments = w.writeHead(rule.Head, rule.Default, isExpandedConst, o, comments)

// this excludes partial sets UNLESS `contains` is used
partialSetException := useContainsKW || rule.Head.Value != nil
partialSetException := o.contains || rule.Head.Value != nil

if len(rule.Body) == 0 || isExpandedConst {
w.endLine()
return comments
}

if useIf && partialSetException {
if o.ifs && partialSetException {
w.write(" if")
if len(rule.Body) == 1 {
if rule.Body[0].Location.Row == rule.Head.Location.Row {
w.write(" ")
comments = w.writeExpr(rule.Body[0], comments)
w.endLine()
if rule.Else != nil {
comments = w.writeElse(rule, useContainsKW, useIf, comments)
comments = w.writeElse(rule, o, comments)
}
return comments
}
Expand Down Expand Up @@ -357,12 +373,12 @@ func (w *writer) writeRule(rule *ast.Rule, isElse, useContainsKW, useIf bool, co
w.startLine()
w.write("}")
if rule.Else != nil {
comments = w.writeElse(rule, useContainsKW, useIf, comments)
comments = w.writeElse(rule, o, comments)
}
return comments
}

func (w *writer) writeElse(rule *ast.Rule, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
func (w *writer) writeElse(rule *ast.Rule, o fmtOpts, comments []*ast.Comment) []*ast.Comment {
// If there was nothing else on the line before the "else" starts
// then preserve this style of else block, otherwise it will be
// started as an "inline" else eg:
Expand Down Expand Up @@ -424,15 +440,22 @@ func (w *writer) writeElse(rule *ast.Rule, useContainsKW, useIf bool, comments [
rule.Else.Head.Value.Location = rule.Else.Head.Location
}

return w.writeRule(rule.Else, true, useContainsKW, useIf, comments)
return w.writeRule(rule.Else, true, o, comments)
}

func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst bool, o fmtOpts, comments []*ast.Comment) []*ast.Comment {
ref := head.Ref()
if head.Key != nil && head.Value == nil {
ref = ref.GroundPrefix()
}
w.write(ref.String())
if o.refHeads || len(ref) == 1 {
w.write(ref.String())
} else {
w.write(ref[0].String())
w.write("[")
w.write(ref[1].String())
w.write("]")
}

if len(head.Args) > 0 {
w.write("(")
Expand All @@ -444,7 +467,7 @@ func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst, useContai
w.write(")")
}
if head.Key != nil {
if useContainsKW && head.Value == nil {
if o.contains && head.Value == nil {
w.write(" contains ")
comments = w.writeTerm(head.Key, comments)
} else if head.Value == nil { // no `if` for p[x] notation
Expand Down
8 changes: 4 additions & 4 deletions format/testfiles/test.rego.formatted
Expand Up @@ -24,11 +24,11 @@ globals = {
"fizz": "buzz",
}

partial_obj.x = 1
partial_obj["x"] = 1

partial_obj.y = 2
partial_obj["y"] = 2

partial_obj.z = 3
partial_obj["z"] = 3

partial_set["x"]

Expand Down Expand Up @@ -198,7 +198,7 @@ nested_infix {

expanded_const = true

partial_obj.why = true {
partial_obj["why"] = true {
false
}

Expand Down
3 changes: 3 additions & 0 deletions format/testfiles/test_issue_5449.rego
@@ -0,0 +1,3 @@
package demo

foo["bar"] = "baz" { input }
5 changes: 5 additions & 0 deletions format/testfiles/test_issue_5449.rego.formatted
@@ -0,0 +1,5 @@
package demo

foo["bar"] = "baz" {
input
}
9 changes: 9 additions & 0 deletions format/testfiles/test_issue_5449_with_contains_ref_rule.rego
@@ -0,0 +1,9 @@
# This is the same as test_issue_5449.rego, but with another rule
# that gives the formatter the assurance that using ref rules is OK
package demo

import future.keywords.contains

foo["bar"] = "baz" { input }

a.deep contains "ref"
@@ -0,0 +1,11 @@
# This is the same as test_issue_5449.rego, but with another rule
# that gives the formatter the assurance that using ref rules is OK
package demo

import future.keywords.contains

foo.bar = "baz" {
input
}

a.deep contains "ref"
7 changes: 7 additions & 0 deletions format/testfiles/test_issue_5449_with_ref_rule.rego
@@ -0,0 +1,7 @@
# This is the same as test_issue_5449.rego, but with a rule that gives
# the formatter the assurance that using ref rules is OK
package demo

foo["bar"] = "baz" { input }

a.deep.ref := true
9 changes: 9 additions & 0 deletions format/testfiles/test_issue_5449_with_ref_rule.rego.formatted
@@ -0,0 +1,9 @@
# This is the same as test_issue_5449.rego, but with a rule that gives
# the formatter the assurance that using ref rules is OK
package demo

foo.bar = "baz" {
input
}

a.deep.ref := true

0 comments on commit c0866e8

Please sign in to comment.