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
31 changes: 28 additions & 3 deletions v1/ast/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1366,6 +1366,21 @@ func (c *Compiler) checkRuleConflicts() {
}
}

// Functions cannot exist within a rule's dynamic extent, as there is no valid
// evaluation scenario for this right now: it will return an error or panic.
// NB(sr): This is something we may overcome later -- but for now, it's better
// to return an error than to fail in hard-to-understand ways.
if conflicts == nil && len(node.Children) > 0 {
for _, rule := range node.Values {
if !rule.Ref().IsGround() {
if funcConflicts := node.flattenChildFunctions(); len(funcConflicts) > 0 {
conflicts = funcConflicts
}
break
}
}
}

switch {
case conflicts != nil:
return !c.err(NewError(TypeErr, node.Values[0].Loc(), "rule %v conflicts with %v", name, conflicts))
Expand Down Expand Up @@ -4236,12 +4251,22 @@ func treeNodeFromRef(ref Ref, rule *Rule) *TreeNode {

// flattenChildren flattens all children's rule refs into a sorted array.
func (n *TreeNode) flattenChildren() []Ref {
return n.flattenMatchingChildren(func(_ *Rule) bool { return true })
}

// flattenChildFunctions is like flattenChildren but only collects functions (rules with args).
func (n *TreeNode) flattenChildFunctions() []Ref {
return n.flattenMatchingChildren(func(r *Rule) bool { return r.isFunction() })
}

func (n *TreeNode) flattenMatchingChildren(f func(*Rule) bool) []Ref {
ret := newRefSet()
for _, sub := range n.Children { // we only want the children, so don't use n.DepthFirst() right away
sub.DepthFirst(func(x *TreeNode) bool {
for _, r := range x.Values {
rule := r
ret.AddPrefix(rule.Ref())
for _, rule := range x.Values {
if f(rule) {
ret.AddPrefix(rule.Ref())
}
}
return false
})
Expand Down
36 changes: 36 additions & 0 deletions v1/ast/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2331,6 +2331,42 @@ func TestCompilerCheckRuleConflictsDotsInRuleHeads(t *testing.T) {
`),
err: "rego_type_error: rule data.pkg.p.q conflicts with [data.pkg.p.q.r]",
},
{
note: "function within dynamic extent of rule",
modules: modules(
`package pkg
a[x].c := i if some i, x in ["one", "two"]
a.b.d(x) := x
`),
err: "rego_type_error: rule data.pkg.a[x].c conflicts with [data.pkg.a.b.d]",
},
{
note: "function within dynamic extent of rule (different packages)",
modules: modules(
`package pkg
a[x].c := i if some i, x in ["one", "two"]`,
`package pkg.a.b
d(x) := x
`),
err: "rego_type_error: rule data.pkg.a[x].c conflicts with [data.pkg.a.b.d]",
},
{
note: "function within dynamic extent, deeper nesting",
modules: modules(
`package pkg
p[x].q if x := "a"
p.r.s.t(x) := x
`),
err: "rego_type_error: rule data.pkg.p[x].q conflicts with [data.pkg.p.r.s.t]",
},
{
note: "non-function rule within dynamic extent (no conflict)",
modules: modules(
`package pkg
p[x].q if x := "a"
p.r.s := 1
`),
},
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
Expand Down