mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
2378494a23
Mostly automated fixes from running: ``` go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest --fix ./... ``` But carefully reviewed, and several fixes reverted as they looked like they potentially could be less performant, and in a few cases due to bugs in the analyzer that changed semantics of the code. Will report these upstream. Mostly good fixes though! Signed-off-by: Anders Eknert <anders.eknert@apple.com>
16103 lines
378 KiB
Go
16103 lines
378 KiB
Go
// Copyright 2016 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package ast
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/open-policy-agent/opa/v1/metrics"
|
|
"github.com/open-policy-agent/opa/v1/types"
|
|
"github.com/open-policy-agent/opa/v1/util"
|
|
)
|
|
|
|
func TestOutputVarsForNode(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
query string
|
|
arities map[string]int
|
|
extraSafe string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "single var",
|
|
query: "x",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "trivial eq",
|
|
query: "x = 1",
|
|
exp: "{x}",
|
|
},
|
|
{
|
|
note: "negation",
|
|
query: "not x = 1",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "embedded array",
|
|
query: "[x, [1]] = [1, [y]]",
|
|
exp: "{x, y}",
|
|
},
|
|
{
|
|
note: "embedded sets",
|
|
query: "{x, [1]} = {1, [y]}",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "embedded object values",
|
|
query: `{"foo": x, "bar": {"baz": 1}} = {"foo": 1, "bar": {"baz": y}}`,
|
|
exp: "{x, y}",
|
|
},
|
|
{
|
|
note: "object keys are like sets",
|
|
query: `{"foo": x} = {y: 1}`,
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "built-ins",
|
|
query: `count([1,2,3], x)`,
|
|
exp: "{x}",
|
|
arities: map[string]int{"count": 1},
|
|
},
|
|
{
|
|
note: "built-ins - input args",
|
|
query: `count(x)`,
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "functions - no arity",
|
|
query: `f(1,x)`,
|
|
arities: map[string]int{},
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "functions",
|
|
query: `f(1,x)`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: "{x}",
|
|
},
|
|
{
|
|
note: "functions - input args",
|
|
query: `f(1,x)`,
|
|
arities: map[string]int{"f": 2},
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "functions - embedded refs",
|
|
query: `f(data.p[x], y)`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `{x, y}`,
|
|
},
|
|
{
|
|
note: "functions - skip ref head",
|
|
query: `f(x[1])`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "functions - skip sets",
|
|
query: `f(1, {x})`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "functions - skip object keys",
|
|
query: `f(1, {x: 1})`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "functions - skip closures",
|
|
query: `f(1, {x | x = 1})`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "functions - unsafe input",
|
|
query: `f(x, y)`,
|
|
arities: map[string]int{"f": 1},
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "with keyword",
|
|
query: "1 with input as y",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "with keyword - unsafe",
|
|
query: "x = 1 with input as y",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "with keyword - safe",
|
|
query: "x = 1 with input as y",
|
|
extraSafe: "{y}",
|
|
exp: "{x}",
|
|
},
|
|
{
|
|
note: "ref operand",
|
|
query: "data.p[x]",
|
|
exp: "{x}",
|
|
},
|
|
{
|
|
note: "ref operand - unsafe head",
|
|
query: "p[x]",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "ref operand - negation",
|
|
query: "not data.p[x]",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "ref operand - nested",
|
|
query: "data.p[data.q[x]]",
|
|
exp: "{x}",
|
|
},
|
|
{
|
|
note: "comprehension",
|
|
query: "[x | x = 1]",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "comprehension containing safe ref",
|
|
query: "[x | data.p[x]]",
|
|
exp: "set()",
|
|
},
|
|
{
|
|
note: "accumulate on exprs",
|
|
query: "x = 1; y = x; z = y",
|
|
exp: "{x, y, z}",
|
|
},
|
|
{
|
|
note: "composite head",
|
|
query: "{1, 2}[1] = x",
|
|
exp: `{x}`,
|
|
},
|
|
{
|
|
note: "composite head",
|
|
query: "x = 1; {x, 2}[1] = y",
|
|
exp: `{x, y}`,
|
|
},
|
|
{
|
|
note: "composite head",
|
|
query: "{x, 2}[1] = y",
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "nested function calls",
|
|
query: `z = "abc"; x = split(z, "")[y]`,
|
|
exp: `{x, y, z}`,
|
|
},
|
|
{
|
|
note: "unsafe nested function calls",
|
|
query: `z = "abc"; x = split(z, a)[y]`,
|
|
exp: `{z}`,
|
|
},
|
|
{
|
|
note: "every: simple: no output vars",
|
|
query: `every k, v in [1, 2] { k < v }`,
|
|
exp: `set()`,
|
|
},
|
|
{
|
|
note: "every: output vars in domain",
|
|
query: `xs = []; every k, v in xs[i] { k < v }`,
|
|
exp: `{xs, i}`,
|
|
},
|
|
{
|
|
note: "every: output vars in body",
|
|
query: `every k, v in [] { k < v; i = 1 }`,
|
|
exp: `set()`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
body, err := ParseBodyWithOpts(tc.query, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
arity := func(r Ref) int {
|
|
a, ok := tc.arities[r.String()]
|
|
if !ok {
|
|
return -1
|
|
}
|
|
return a
|
|
}
|
|
|
|
safe := ReservedVars.Copy()
|
|
|
|
if tc.extraSafe != "" {
|
|
MustParseTerm(tc.extraSafe).Value.(Set).Foreach(func(x *Term) {
|
|
safe.Add(x.Value.(Var))
|
|
})
|
|
}
|
|
|
|
vs := NewSet()
|
|
|
|
for v := range outputVarsForBody(body, arity, safe, nil) {
|
|
vs.Add(NewTerm(v))
|
|
}
|
|
|
|
exp := MustParseTerm(tc.exp)
|
|
|
|
if exp.Value.Compare(vs) != 0 {
|
|
t.Fatalf("Expected %v but got %v", exp, vs)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestModuleTree(t *testing.T) {
|
|
|
|
mods := getCompilerTestModules() // 7 modules
|
|
mods["system-mod"] = MustParseModule(`
|
|
package system.foo
|
|
|
|
p = 1
|
|
`)
|
|
mods["non-system-mod"] = MustParseModule(`
|
|
package user.system
|
|
|
|
p = 1
|
|
`)
|
|
mods["dots-in-heads"] = MustParseModule(`
|
|
package dots
|
|
|
|
a.b.c = 12
|
|
d.e.f.g = 34
|
|
`)
|
|
tree := NewModuleTree(mods)
|
|
expectedSize := 10
|
|
|
|
if tree.Size() != expectedSize {
|
|
t.Fatalf("Expected %v but got %v modules", expectedSize, tree.Size())
|
|
}
|
|
|
|
if !tree.Children[Var("data")].Children[String("system")].Hide {
|
|
t.Fatalf("Expected system node to be hidden")
|
|
}
|
|
|
|
if tree.Children[Var("data")].Children[String("system")].Children[String("foo")].Hide {
|
|
t.Fatalf("Expected system.foo node to be visible")
|
|
}
|
|
|
|
if tree.Children[Var("data")].Children[String("user")].Children[String("system")].Hide {
|
|
t.Fatalf("Expected user.system node to be visible")
|
|
}
|
|
}
|
|
|
|
func TestCompilerGetExports(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
exports map[string][]string
|
|
}{
|
|
{
|
|
note: "simple",
|
|
modules: modules(`package p
|
|
r = 1`),
|
|
exports: map[string][]string{"data.p": {"r"}},
|
|
},
|
|
{
|
|
note: "simple single-value ref rule",
|
|
modules: modules(`package p
|
|
q.r.s = 1`),
|
|
exports: map[string][]string{"data.p": {"q.r.s"}},
|
|
},
|
|
{
|
|
note: "var key single-value ref rule",
|
|
modules: modules(`package p
|
|
q.r[s] = 1 if { s := "foo" }`),
|
|
exports: map[string][]string{"data.p": {"q.r"}},
|
|
},
|
|
{
|
|
note: "simple multi-value ref rule",
|
|
modules: modules(`package p
|
|
q.r.s contains 1 if { true }`),
|
|
exports: map[string][]string{"data.p": {"q.r.s"}},
|
|
},
|
|
{
|
|
note: "two simple, multiple rules",
|
|
modules: modules(`package p
|
|
r = 1
|
|
s = 11`,
|
|
`package q
|
|
x = 2
|
|
y = 22`),
|
|
exports: map[string][]string{"data.p": {"r", "s"}, "data.q": {"x", "y"}},
|
|
},
|
|
{
|
|
note: "ref head + simple, multiple rules",
|
|
modules: modules(`package p.a.b.c
|
|
r = 1
|
|
s = 11`,
|
|
`package q
|
|
a.b.x = 2
|
|
a.b.c.y = 22`),
|
|
exports: map[string][]string{
|
|
"data.p.a.b.c": {"r", "s"},
|
|
"data.q": {"a.b.x", "a.b.c.y"},
|
|
},
|
|
},
|
|
{
|
|
note: "two ref head, multiple rules",
|
|
modules: modules(`package p.a.b.c
|
|
r = 1
|
|
s = 11`,
|
|
`package p
|
|
a.b.x = 2
|
|
a.b.c.y = 22`),
|
|
exports: map[string][]string{
|
|
"data.p.a.b.c": {"r", "s"},
|
|
"data.p": {"a.b.x", "a.b.c.y"},
|
|
},
|
|
},
|
|
{
|
|
note: "single-value rule with number key",
|
|
modules: modules(`package p
|
|
q[1] = 1
|
|
q[2] = 2`),
|
|
exports: map[string][]string{
|
|
"data.p": {"q[1]", "q[2]"}, // TODO(sr): is this really what we want?
|
|
},
|
|
},
|
|
{
|
|
note: "single-value (ref) rule with number key",
|
|
modules: modules(`package p
|
|
a.b.q[1] = 1
|
|
a.b.q[2] = 2`),
|
|
exports: map[string][]string{
|
|
"data.p": {"a.b.q[1]", "a.b.q[2]"},
|
|
},
|
|
},
|
|
{
|
|
note: "single-value (ref) rule with var key",
|
|
modules: modules(`package p
|
|
a.b.q[x] = y if { x := 1; y := true }
|
|
a.b.q[2] = 2`),
|
|
exports: map[string][]string{
|
|
"data.p": {"a.b.q", "a.b.q[2]"}, // TODO(sr): GroundPrefix? right thing here?
|
|
},
|
|
},
|
|
{ // NOTE(sr): An ast.Module can be constructed in various ways, this is to assert that
|
|
// our compilation process doesn't explode here if we're fed a Rule that has no Ref.
|
|
note: "synthetic",
|
|
modules: func() []*Module {
|
|
ms := modules(`package p
|
|
r = 1`)
|
|
ms[0].Rules[0].Head.Reference = nil
|
|
return ms
|
|
}(),
|
|
exports: map[string][]string{"data.p": {"r"}},
|
|
},
|
|
// TODO(sr): add multi-val rule, and ref-with-var single-value rule.
|
|
}
|
|
|
|
hashMap := func(ms map[string][]string) *util.HasherMap[Ref, []Ref] {
|
|
rules := util.NewHasherMap[Ref, []Ref](RefEqual)
|
|
for r, rs := range ms {
|
|
refs := make([]Ref, len(rs))
|
|
for i := range rs {
|
|
refs[i] = toRef(rs[i])
|
|
}
|
|
rules.Put(MustParseRef(r), refs)
|
|
}
|
|
return rules
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
for i, m := range tc.modules {
|
|
c.Modules[strconv.Itoa(i)] = m
|
|
c.sorted = append(c.sorted, strconv.Itoa(i))
|
|
}
|
|
if exp, act := hashMap(tc.exports), c.getExports(); !refMapEqual(exp, act) {
|
|
t.Errorf("expected %v, got %v", exp, act)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func refMapEqual(a, b *util.HasherMap[Ref, []Ref]) bool {
|
|
if a.Len() != b.Len() {
|
|
return false
|
|
}
|
|
return !a.Iter(func(k Ref, v []Ref) bool {
|
|
v2, ok := b.Get(k)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if !refSliceEqual(v, v2) {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
func toRef(s string) Ref {
|
|
switch t := MustParseTerm(s).Value.(type) {
|
|
case Var:
|
|
return Ref{NewTerm(t)}
|
|
case Ref:
|
|
return t
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckRuleHeadRefs(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
expected *Rule
|
|
err string
|
|
}{
|
|
{
|
|
note: "ref contains var",
|
|
modules: modules(
|
|
`package x
|
|
p.q[i].r = 1 if { i := 10 }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with var key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[i] if { i := 10 }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with var key and value",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[i] = j if { i := 10; j := 11 }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with var key and static value",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[i] = "ten" if { i := 10 }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with number key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[1] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with boolean key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[true] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with null key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[null] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with set literal key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[set()] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with array literal key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[[]] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with object literal key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[{}] if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: ref is single-value rule with ref key",
|
|
modules: modules(
|
|
`package x
|
|
x := [1,2,3]
|
|
p.q.r[x[i]] if { i := 0}`,
|
|
),
|
|
},
|
|
{
|
|
note: "invalid: ref in ref",
|
|
modules: modules(
|
|
`package x
|
|
p.q[arr[0]].r if { i := 10 }`,
|
|
),
|
|
},
|
|
{
|
|
note: "invalid: non-string in ref (not last position)",
|
|
modules: modules(
|
|
`package x
|
|
p.q[10].r if { true }`,
|
|
),
|
|
},
|
|
{
|
|
note: "valid: multi-value with var key",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r contains i if i := 10`,
|
|
),
|
|
},
|
|
{
|
|
note: "rewrite: single-value with non-var key (ref)",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r[y.z] if y := {"z": "a"}`,
|
|
),
|
|
expected: MustParseRule(`p.q.r[__local0__] { y := {"z": "a"}; __local0__ = y.z }`),
|
|
},
|
|
{
|
|
note: "rewrite: single-value with non-var ref term",
|
|
modules: modules(
|
|
`package x
|
|
p.q[y.z].r if y := {"z": "a"}`,
|
|
),
|
|
expected: MustParseRule(`p.q[__local0__].r { y := {"z": "a"}; __local0__ = y.z }`),
|
|
},
|
|
{
|
|
note: "rewrite: single-value with non-var ref term and key",
|
|
modules: modules(
|
|
`package x
|
|
p.q[a.b][c.d] if y := {"z": "a"}`,
|
|
),
|
|
expected: MustParseRule(`p.q[__local0__][__local1__] { y := {"z": "a"}; __local0__ = a.b; __local1__ = c.d }`),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
mods := make(map[string]*Module, len(tc.modules))
|
|
for i, m := range tc.modules {
|
|
mods[strconv.Itoa(i)] = m
|
|
}
|
|
c := NewCompiler()
|
|
c.Modules = mods
|
|
compileStages(c, StageRewriteRuleHeadRefs)
|
|
if tc.err != "" {
|
|
assertCompilerErrorStrings(t, c, []string{tc.err})
|
|
} else {
|
|
if len(c.Errors) > 0 {
|
|
t.Fatalf("expected no errors, got %v", c.Errors)
|
|
}
|
|
if tc.expected != nil {
|
|
assertRulesEqual(t, tc.expected, mods["0"].Rules[0])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuleTreeWithDotsInHeads(t *testing.T) {
|
|
|
|
// TODO(sr): multi-val with var key in ref
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
size int // expected tree size = number of leaves
|
|
depth int // expected tree depth
|
|
}{
|
|
{
|
|
note: "two modules, same package, one rule each",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1`,
|
|
`package x
|
|
p.q.w = 2`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "two modules, sub-package, one rule each",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1`,
|
|
`package x.p
|
|
q.w.z = 2`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "three modules, sub-package, incl simple rule",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1`,
|
|
`package x.p
|
|
q.w.z = 2`,
|
|
`package x.p.q.w
|
|
y = 3`,
|
|
),
|
|
size: 3,
|
|
},
|
|
{
|
|
note: "simple: two modules",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1`,
|
|
`package y
|
|
p.q.w = 2`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "conflict: one module",
|
|
modules: modules(
|
|
`package q
|
|
p[x] = 1
|
|
p = 2`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "conflict: two modules",
|
|
modules: modules(
|
|
`package q
|
|
p.r.s[x] = 1`,
|
|
`package q.p
|
|
r.s = 2 if true`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "simple: two modules, one using ref head, one package path",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1 if { input == 1 }`,
|
|
`package x.p.q
|
|
r = 2 if { input == 2 }`,
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "conflict: two modules, both using ref head, different package paths",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1 if { input == 1 }`, // x.p.q.r = 1
|
|
`package x.p
|
|
q.r.s = 2 if { input == 2 }`, // x.p.q.r.s = 2
|
|
),
|
|
size: 2,
|
|
},
|
|
{
|
|
note: "overlapping: one module, two ref head",
|
|
modules: modules(
|
|
`package x
|
|
p.q.r = 1
|
|
p.q.w.v = 2`,
|
|
),
|
|
size: 2,
|
|
depth: 6,
|
|
},
|
|
{
|
|
note: "last ref term != string",
|
|
modules: modules(
|
|
`package x
|
|
p.q.w[1] = 2
|
|
p.q.w[{"foo": "baz"}] = 20
|
|
p.q.x[true] = false
|
|
p.q.x[y] = y if { y := "y" }`,
|
|
),
|
|
size: 4,
|
|
depth: 6,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
for i, m := range tc.modules {
|
|
c.Modules[strconv.Itoa(i)] = m
|
|
c.sorted = append(c.sorted, strconv.Itoa(i))
|
|
}
|
|
compileStages(c, StageSetRuleTree)
|
|
if len(c.Errors) > 0 {
|
|
t.Fatal(c.Errors)
|
|
}
|
|
tree := c.RuleTree
|
|
tree.DepthFirst(func(n *TreeNode) bool {
|
|
t.Log(n)
|
|
if !sort.SliceIsSorted(n.Sorted, func(i, j int) bool {
|
|
return n.Sorted[i].Compare(n.Sorted[j]) < 0
|
|
}) {
|
|
t.Errorf("expected sorted to be sorted: %v", n.Sorted)
|
|
}
|
|
return false
|
|
})
|
|
if tc.depth > 0 {
|
|
if exp, act := tc.depth, depth(tree); exp != act {
|
|
t.Errorf("expected tree depth %d, got %d", exp, act)
|
|
}
|
|
}
|
|
if exp, act := tc.size, tree.Size(); exp != act {
|
|
t.Errorf("expected tree size %d, got %d", exp, act)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuleIndices(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
exp map[string][]Ref
|
|
}{
|
|
{
|
|
note: "regression test for #6930 (no if)",
|
|
modules: modules(
|
|
`package test
|
|
|
|
p.q contains "foo"
|
|
|
|
p[q] := r if {
|
|
q := "bar"
|
|
r := "baz"
|
|
}`,
|
|
),
|
|
exp: map[string][]Ref{
|
|
"data.test.p": {
|
|
MustParseRef("p.q"),
|
|
MustParseRef("p[__local0__]"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "regression test for #6930 (if)",
|
|
modules: modules(
|
|
`package test
|
|
|
|
p.q contains "foo"
|
|
|
|
p[q] := r if {
|
|
q := "bar"
|
|
r := "baz"
|
|
}`,
|
|
),
|
|
exp: map[string][]Ref{
|
|
"data.test.p": {
|
|
MustParseRef("p.q"),
|
|
MustParseRef("p[__local0__]"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
for i, m := range tc.modules {
|
|
c.Modules[strconv.Itoa(i)] = m
|
|
c.sorted = append(c.sorted, strconv.Itoa(i))
|
|
}
|
|
compileStages(c, StageBuildRuleIndices)
|
|
t.Log(c.RuleTree.Dump())
|
|
|
|
for k, expIndex := range tc.exp {
|
|
kref := MustParseRef(k)
|
|
i := c.RuleIndex(kref)
|
|
if i == nil {
|
|
t.Fatalf("expected rule indices for %v", k)
|
|
}
|
|
index := i.(*baseDocEqIndex)
|
|
for _, expRef := range expIndex {
|
|
found := false
|
|
for _, r := range index.root.rules {
|
|
if r.rule.Head.Ref().Equal(expRef) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("expected rule %v in index for %v", expRef, k)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuleTreeWithVars(t *testing.T) {
|
|
opts := ParserOptions{
|
|
RegoVersion: RegoV0,
|
|
AllFutureKeywords: true,
|
|
}
|
|
|
|
t.Run("simple single-value rule", func(t *testing.T) {
|
|
mod0 := `package a.b
|
|
c.d.e = 1 if true`
|
|
|
|
mods := map[string]*Module{"0.rego": MustParseModuleWithOpts(mod0, opts)}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
node := tree.Find(MustParseRef("data.a.b.c.d.e"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 1, len(node.Values); exp != act {
|
|
t.Errorf("expected %d values, found %d", exp, act)
|
|
}
|
|
if exp, act := 0, len(node.Children); exp != act {
|
|
t.Errorf("expected %d children, found %d", exp, act)
|
|
}
|
|
if exp, act := MustParseRef("c.d.e"), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("two single-value rules", func(t *testing.T) {
|
|
mod0 := `package a.b
|
|
c.d.e = 1 if true`
|
|
mod1 := `package a.b.c
|
|
d.e = 2 if true`
|
|
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
"1.rego": MustParseModuleWithOpts(mod1, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
node := tree.Find(MustParseRef("data.a.b.c.d.e"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 2, len(node.Values); exp != act {
|
|
t.Errorf("expected %d values, found %d", exp, act)
|
|
}
|
|
if exp, act := 0, len(node.Children); exp != act {
|
|
t.Errorf("expected %d children, found %d", exp, act)
|
|
}
|
|
if exp, act := MustParseRef("c.d.e"), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if exp, act := MustParseRef("d.e"), node.Values[1].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("one multi-value rule, one single-value, with var", func(t *testing.T) {
|
|
mod0 := `package a.b
|
|
c.d.e.g contains 1 if true`
|
|
mod1 := `package a.b.c
|
|
d.e.f = 2 if true`
|
|
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
"1.rego": MustParseModuleWithOpts(mod1, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
// var-key rules should be included in the results
|
|
node := tree.Find(MustParseRef("data.a.b.c.d.e.g"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 1, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d", exp, act)
|
|
}
|
|
if exp, act := 0, len(node.Children); exp != act {
|
|
t.Fatalf("expected %d children, found %d", exp, act)
|
|
}
|
|
node = tree.Find(MustParseRef("data.a.b.c.d.e.f"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 1, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d", exp, act)
|
|
}
|
|
if exp, act := MustParseRef("d.e.f"), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("two multi-value rules, back compat", func(t *testing.T) {
|
|
mod0 := `package a
|
|
b[c] { c := "foo" }`
|
|
mod1 := `package a
|
|
b[d] { d := "bar" }`
|
|
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
"1.rego": MustParseModuleWithOpts(mod1, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
node := tree.Find(MustParseRef("data.a.b"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 2, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d: %v", exp, act, node.Values)
|
|
}
|
|
if exp, act := 0, len(node.Children); exp != act {
|
|
t.Errorf("expected %d children, found %d", exp, act)
|
|
}
|
|
if exp, act := (Ref{VarTerm("b")}), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if act := node.Values[0].Head.Value; act != nil {
|
|
t.Errorf("expected rule value nil, found %v", act)
|
|
}
|
|
if exp, act := VarTerm("c"), node.Values[0].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
if exp, act := (Ref{VarTerm("b")}), node.Values[1].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if act := node.Values[1].Head.Value; act != nil {
|
|
t.Errorf("expected rule value nil, found %v", act)
|
|
}
|
|
if exp, act := VarTerm("d"), node.Values[1].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("two multi-value rules, back compat with short style", func(t *testing.T) {
|
|
mod0 := `package a
|
|
b[1]`
|
|
mod1 := `package a
|
|
b[2]`
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
"1.rego": MustParseModuleWithOpts(mod1, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
node := tree.Find(MustParseRef("data.a.b"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 2, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d: %v", exp, act, node.Values)
|
|
}
|
|
if exp, act := 0, len(node.Children); exp != act {
|
|
t.Errorf("expected %d children, found %d", exp, act)
|
|
}
|
|
if exp, act := (Ref{VarTerm("b")}), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if act := node.Values[0].Head.Value; act != nil {
|
|
t.Errorf("expected rule value nil, found %v", act)
|
|
}
|
|
if exp, act := IntNumberTerm(1), node.Values[0].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
if exp, act := (Ref{VarTerm("b")}), node.Values[1].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if act := node.Values[1].Head.Value; act != nil {
|
|
t.Errorf("expected rule value nil, found %v", act)
|
|
}
|
|
if exp, act := IntNumberTerm(2), node.Values[1].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("two single-value rules, back compat with short style", func(t *testing.T) {
|
|
mod0 := `package a
|
|
b[1] = 1`
|
|
mod1 := `package a
|
|
b[2] = 2`
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
"1.rego": MustParseModuleWithOpts(mod1, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
// branch point
|
|
node := tree.Find(MustParseRef("data.a.b"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 0, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d: %v", exp, act, node.Values)
|
|
}
|
|
if exp, act := 2, len(node.Children); exp != act {
|
|
t.Fatalf("expected %d children, found %d", exp, act)
|
|
}
|
|
|
|
// branch 1
|
|
node = tree.Find(MustParseRef("data.a.b[1]"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 1, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d: %v", exp, act, node.Values)
|
|
}
|
|
if exp, act := MustParseRef("b[1]"), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if exp, act := IntNumberTerm(1), node.Values[0].Head.Value; !exp.Equal(act) {
|
|
t.Errorf("expected rule value %v, found %v", exp, act)
|
|
}
|
|
if exp, act := IntNumberTerm(1), node.Values[0].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
|
|
// branch 2
|
|
node = tree.Find(MustParseRef("data.a.b[2]"))
|
|
if node == nil {
|
|
t.Fatal("expected non-nil leaf node")
|
|
}
|
|
if exp, act := 1, len(node.Values); exp != act {
|
|
t.Fatalf("expected %d values, found %d: %v", exp, act, node.Values)
|
|
}
|
|
if exp, act := MustParseRef("b[2]"), node.Values[0].Head.Ref(); !exp.Equal(act) {
|
|
t.Errorf("expected rule ref %v, found %v", exp, act)
|
|
}
|
|
if exp, act := IntNumberTerm(2), node.Values[0].Head.Value; !exp.Equal(act) {
|
|
t.Errorf("expected rule value %v, found %v", exp, act)
|
|
}
|
|
if exp, act := IntNumberTerm(2), node.Values[0].Head.Key; !exp.Equal(act) {
|
|
t.Errorf("expected rule key %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
// NOTE(sr): Now this test seems obvious, but it's a bug that had snuck into the
|
|
// NewRuleTree code during development.
|
|
t.Run("root node and data node unhidden if there are no system nodes", func(t *testing.T) {
|
|
mod0 := `package a
|
|
p = 1`
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModuleWithOpts(mod0, opts),
|
|
}
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
|
|
if exp, act := false, tree.Hide; act != exp {
|
|
t.Errorf("expected tree.Hide=%v, got %v", exp, act)
|
|
}
|
|
dataNode := tree.Child(Var("data"))
|
|
if dataNode == nil {
|
|
t.Fatal("expected data node")
|
|
}
|
|
if exp, act := false, dataNode.Hide; act != exp {
|
|
t.Errorf("expected dataNode.Hide=%v, got %v", exp, act)
|
|
}
|
|
})
|
|
}
|
|
|
|
func depth(n *TreeNode) int {
|
|
d := -1
|
|
for _, m := range n.Children {
|
|
if d0 := depth(m); d0 > d {
|
|
d = d0
|
|
}
|
|
}
|
|
return d + 1
|
|
}
|
|
|
|
func TestModuleTreeFilenameOrder(t *testing.T) {
|
|
// NOTE(sr): It doesn't matter that these are conflicting; but that's where it
|
|
// becomes very apparent: before this change, the rule that was reported as
|
|
// "conflicting" was that of either one of the input files, randomly.
|
|
mods := map[string]*Module{
|
|
"0.rego": MustParseModule(`package p
|
|
import rego.v1
|
|
r = 1 if { true }`),
|
|
"1.rego": MustParseModule(`package p
|
|
import rego.v1
|
|
r = 2 if { true }`),
|
|
}
|
|
tree := NewModuleTree(mods)
|
|
vals := tree.Children[Var("data")].Children[String("p")].Modules
|
|
if exp, act := 2, len(vals); exp != act {
|
|
t.Fatalf("expected %d rules, found %d", exp, act)
|
|
}
|
|
mod0 := vals[0]
|
|
mod1 := vals[1]
|
|
if exp, act := IntNumberTerm(1), mod0.Rules[0].Head.Value; !exp.Equal(act) {
|
|
t.Errorf("expected value %v, got %v", exp, act)
|
|
}
|
|
if exp, act := IntNumberTerm(2), mod1.Rules[0].Head.Value; !exp.Equal(act) {
|
|
t.Errorf("expected value %v, got %v", exp, act)
|
|
}
|
|
}
|
|
func TestRuleTree(t *testing.T) {
|
|
|
|
mods := getCompilerTestModules()
|
|
mods["system-mod"] = MustParseModule(`
|
|
package system.foo
|
|
|
|
p = 1
|
|
`)
|
|
mods["non-system-mod"] = MustParseModule(`
|
|
package user.system
|
|
|
|
p = 1`)
|
|
mods["mod-incr"] = MustParseModule(`
|
|
package a.b.c
|
|
import rego.v1
|
|
|
|
s contains 1 if { true }
|
|
s contains 2 if { true }`,
|
|
)
|
|
|
|
mods["dots-in-heads"] = MustParseModule(`
|
|
package dots
|
|
|
|
a.b.c = 12
|
|
d.e.f.g = 34
|
|
`)
|
|
|
|
tree := NewRuleTree(NewModuleTree(mods))
|
|
expectedNumRules := 25
|
|
|
|
if tree.Size() != expectedNumRules {
|
|
t.Errorf("Expected %v but got %v rules", expectedNumRules, tree.Size())
|
|
}
|
|
|
|
// Check that empty packages are represented as leaves with no rules.
|
|
node := tree.Children[Var("data")].Children[String("a")].Children[String("b")].Children[String("empty")]
|
|
if node == nil || len(node.Children) != 0 || len(node.Values) != 0 {
|
|
t.Fatalf("Unexpected nil value or non-empty leaf of non-leaf node: %v", node)
|
|
}
|
|
|
|
// Check that root node is not hidden
|
|
if exp, act := false, tree.Hide; act != exp {
|
|
t.Errorf("expected tree.Hide=%v, got %v", exp, act)
|
|
}
|
|
|
|
system := tree.Child(Var("data")).Child(String("system"))
|
|
if !system.Hide {
|
|
t.Fatalf("Expected system node to be hidden: %v", system)
|
|
}
|
|
|
|
if system.Child(String("foo")).Hide {
|
|
t.Fatalf("Expected system.foo node to be visible")
|
|
}
|
|
|
|
user := tree.Child(Var("data")).Child(String("user")).Child(String("system"))
|
|
if user.Hide {
|
|
t.Fatalf("Expected user.system node to be visible")
|
|
}
|
|
|
|
if !tree.isVirtual(MustParseRef("data.a.b.empty")) {
|
|
t.Fatal("Expected data.a.b.empty to be virtual")
|
|
}
|
|
|
|
abc := tree.Children[Var("data")].Children[String("a")].Children[String("b")].Children[String("c")]
|
|
exp := []Value{String("p"), String("q"), String("r"), String("s"), String("z")}
|
|
|
|
if len(abc.Sorted) != len(exp) {
|
|
t.Fatal("expected", exp, "but got", abc)
|
|
}
|
|
|
|
for i := range exp {
|
|
if exp[i].Compare(abc.Sorted[i]) != 0 {
|
|
t.Fatal("expected", exp, "but got", abc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompilerEmpty(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Compile(nil)
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
func TestCompilerExample(t *testing.T) {
|
|
c := NewCompiler()
|
|
m := MustParseModuleWithOpts(testModule, ParserOptions{AllFutureKeywords: true})
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
func TestCompilerWithStageAfter(t *testing.T) {
|
|
t.Run("after failing means overall failure", func(t *testing.T) {
|
|
c := NewCompiler().WithStageAfter(
|
|
"CheckRecursion",
|
|
CompilerStageDefinition{"MockStage", "mock_stage",
|
|
func(*Compiler) *Error { return NewError(CompileErr, &Location{}, "mock stage error") }},
|
|
)
|
|
m := MustParseModuleWithOpts(testModule, ParserOptions{AllFutureKeywords: true})
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
|
|
if !c.Failed() {
|
|
t.Errorf("Expected compilation error")
|
|
}
|
|
})
|
|
|
|
t.Run("first 'after' failure inhibits other 'after' stages", func(t *testing.T) {
|
|
c := NewCompiler().
|
|
WithStageAfter("CheckRecursion",
|
|
CompilerStageDefinition{"MockStage", "mock_stage",
|
|
func(*Compiler) *Error { return NewError(CompileErr, &Location{}, "mock stage error") }}).
|
|
WithStageAfter("CheckRecursion",
|
|
CompilerStageDefinition{"MockStage2", "mock_stage2",
|
|
func(*Compiler) *Error { return NewError(CompileErr, &Location{}, "mock stage error two") }},
|
|
)
|
|
m := MustParseModule(`package p
|
|
q := true`)
|
|
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
|
|
if !c.Failed() {
|
|
t.Errorf("Expected compilation error")
|
|
}
|
|
if exp, act := 1, len(c.Errors); exp != act {
|
|
t.Errorf("expected %d errors, got %d: %v", exp, act, c.Errors)
|
|
}
|
|
})
|
|
|
|
t.Run("'after' failure inhibits other ordinary stages", func(t *testing.T) {
|
|
c := NewCompiler().
|
|
WithStageAfter("CheckRecursion",
|
|
CompilerStageDefinition{"MockStage", "mock_stage",
|
|
func(*Compiler) *Error { return NewError(CompileErr, &Location{}, "mock stage error") }})
|
|
m := MustParseModule(`package p
|
|
import rego.v1
|
|
|
|
q if {
|
|
1 == "a" # would fail "CheckTypes", the next stage
|
|
}
|
|
`)
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
|
|
if !c.Failed() {
|
|
t.Errorf("Expected compilation error")
|
|
}
|
|
if exp, act := 1, len(c.Errors); exp != act {
|
|
t.Errorf("expected %d errors, got %d: %v", exp, act, c.Errors)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompilerFunctions(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
note: "multiple input types",
|
|
modules: []string{`package x
|
|
|
|
f([x]) = y {
|
|
y = x
|
|
}
|
|
|
|
f({"foo": x}) = y {
|
|
y = x
|
|
}`},
|
|
},
|
|
{
|
|
note: "multiple input types",
|
|
modules: []string{`package x
|
|
|
|
f([x]) = y {
|
|
y = x
|
|
}
|
|
|
|
f([[x]]) = y {
|
|
y = x
|
|
}`},
|
|
},
|
|
{
|
|
note: "constant input",
|
|
modules: []string{`package x
|
|
|
|
f(1) = y {
|
|
y = "foo"
|
|
}
|
|
|
|
f(2) = y {
|
|
y = "bar"
|
|
}`},
|
|
},
|
|
{
|
|
note: "constant input",
|
|
modules: []string{`package x
|
|
|
|
f(1, x) = y {
|
|
y = x
|
|
}
|
|
|
|
f(x, y) = z {
|
|
z = x+y
|
|
}`},
|
|
},
|
|
{
|
|
note: "constant input",
|
|
modules: []string{`package x
|
|
|
|
f(x, 1) = y {
|
|
y = x
|
|
}
|
|
|
|
f(x, [y]) = z {
|
|
z = x+y
|
|
}`},
|
|
},
|
|
{
|
|
note: "multiple input types (nested)",
|
|
modules: []string{`package x
|
|
|
|
f({"foo": {"bar": x}}) = y {
|
|
y = x
|
|
}
|
|
|
|
f({"foo": [x]}) = y {
|
|
y = x
|
|
}`},
|
|
},
|
|
{
|
|
note: "multiple output types",
|
|
modules: []string{`package x
|
|
|
|
f(1) = y {
|
|
y = "foo"
|
|
}
|
|
|
|
f(2) = y {
|
|
y = 2
|
|
}`},
|
|
},
|
|
{
|
|
note: "namespacing",
|
|
modules: []string{
|
|
`package x
|
|
|
|
f(x) = y {
|
|
data.y.f[x] = y
|
|
}`,
|
|
`package y
|
|
|
|
f[x] = y {
|
|
y = "bar"
|
|
x = "foo"
|
|
}`,
|
|
},
|
|
},
|
|
{
|
|
note: "implicit value",
|
|
modules: []string{
|
|
`package x
|
|
|
|
f(x) {
|
|
x = "foo"
|
|
}`},
|
|
},
|
|
{
|
|
note: "resolving",
|
|
modules: []string{
|
|
`package x
|
|
|
|
f(x) = x { true }`,
|
|
|
|
`package y
|
|
|
|
import data.x
|
|
import data.x.f as g
|
|
|
|
p { g(1, a) }
|
|
p { x.f(1, b) }
|
|
p { data.x.f(1, c) }
|
|
`,
|
|
},
|
|
},
|
|
{
|
|
note: "undefined",
|
|
modules: []string{
|
|
`package x
|
|
|
|
p {
|
|
f(1)
|
|
}`,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
note: "must apply",
|
|
modules: []string{
|
|
`package x
|
|
|
|
f(1)
|
|
|
|
p {
|
|
f
|
|
}
|
|
`,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
note: "must apply",
|
|
modules: []string{
|
|
`package x
|
|
f(1)
|
|
p { f.x }`,
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
note: "call argument ref output vars",
|
|
modules: []string{
|
|
`package x
|
|
|
|
f(x)
|
|
|
|
p { f(data.foo[i]) }`,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var err error
|
|
modules := map[string]*Module{}
|
|
for i, module := range tc.modules {
|
|
name := fmt.Sprintf("mod%d", i)
|
|
modules[name], err = ParseModuleWithOpts(name, module, ParserOptions{RegoVersion: RegoV0})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
c := NewCompiler()
|
|
c.Compile(modules)
|
|
if tc.wantErr && !c.Failed() {
|
|
t.Errorf("Expected compilation error")
|
|
} else if !tc.wantErr && c.Failed() {
|
|
t.Errorf("Unexpected compilation error(s): %v", c.Errors)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerErrorLimit(t *testing.T) {
|
|
modules := map[string]*Module{
|
|
"test": MustParseModule(`package test
|
|
import rego.v1
|
|
|
|
r = y if { y = true; x = z }
|
|
|
|
s[x] = y if {
|
|
z = y + x
|
|
}
|
|
|
|
t contains x if { split(x, y, z) }
|
|
`),
|
|
}
|
|
|
|
c := NewCompiler().SetErrorLimit(2)
|
|
c.Compile(modules)
|
|
|
|
errs := c.Errors
|
|
exp := []string{
|
|
"4:23: rego_unsafe_var_error: var x is unsafe",
|
|
"4:23: rego_unsafe_var_error: var z is unsafe",
|
|
"rego_compile_error: error limit reached",
|
|
}
|
|
|
|
result := make([]string, 0, len(errs))
|
|
for _, err := range errs {
|
|
result = append(result, err.Error())
|
|
}
|
|
|
|
sort.Strings(exp)
|
|
sort.Strings(result)
|
|
if !slices.Equal(exp, result) {
|
|
t.Errorf("Expected errors %v, got %v", exp, result)
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckSafetyHead(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = getCompilerTestModules()
|
|
popts := ParserOptions{AllFutureKeywords: true}
|
|
c.Modules["newMod"] = MustParseModuleWithOpts(`package a.b
|
|
|
|
unboundKey[x1] = y if { q[y] = {"foo": [1, 2, [{"bar": y}]]} }
|
|
unboundVal[y] = x2 if { q[y] = {"foo": [1, 2, [{"bar": y}]]} }
|
|
unboundCompositeVal[y] = [{"foo": x3, "bar": y}] if { q[y] = {"foo": [1, 2, [{"bar": y}]]} }
|
|
unboundCompositeKey contains [{"x": x4}] if { q[y] }
|
|
unboundBuiltinOperator = eq if { 4 = 1 }
|
|
unboundElse if { false } else = else_var if { true }
|
|
c.d.e[x5] if true
|
|
f.g.h[y] = x6 if y := "y"
|
|
i.j.k contains x7 if true
|
|
`, popts)
|
|
compileStages(c, StageCheckSafetyRuleHeads)
|
|
|
|
makeErrMsg := func(v string) string {
|
|
return fmt.Sprintf("rego_unsafe_var_error: var %v is unsafe", v)
|
|
}
|
|
|
|
expected := []string{
|
|
makeErrMsg("x1"),
|
|
makeErrMsg("x2"),
|
|
makeErrMsg("x3"),
|
|
makeErrMsg("x4"),
|
|
makeErrMsg("x5"),
|
|
makeErrMsg("x6"),
|
|
makeErrMsg("x7"),
|
|
makeErrMsg("eq"),
|
|
makeErrMsg("else_var"),
|
|
}
|
|
|
|
result := compilerErrsToStringSlice(c.Errors)
|
|
sort.Strings(expected)
|
|
|
|
if len(result) != len(expected) {
|
|
t.Fatalf("Expected %d:\n%v\nBut got %d:\n%v", len(expected), strings.Join(expected, "\n"), len(result), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if expected[i] != result[i] {
|
|
t.Errorf("Expected %v but got: %v", expected[i], result[i])
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerCheckSafetyBodyReordering(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
body string
|
|
expected string
|
|
}{
|
|
{"noop", `x = 1; x != 0`, `x = 1; x != 0`},
|
|
{"var/ref", `a[i] = x; a = [1, 2, 3, 4]`, `a = [1, 2, 3, 4]; a[i] = x`},
|
|
{"var/ref (nested)", `a = [1, 2, 3, 4]; a[b[i]] = x; b = [0, 0, 0, 0]`, `a = [1, 2, 3, 4]; b = [0, 0, 0, 0]; a[b[i]] = x`},
|
|
{"negation",
|
|
`a = [true, false]; b = [true, false]; not a[i]; b[i]`,
|
|
`a = [true, false]; b = [true, false]; b[i]; not a[i]`},
|
|
{"built-in", `x != 0; count([1, 2, 3], x)`, `count([1, 2, 3], x); x != 0`},
|
|
{"var/var 1", `x = y; z = 1; y = z`, `z = 1; y = z; x = y`},
|
|
{"var/var 2", `x = y; 1 = z; z = y`, `1 = z; z = y; x = y`},
|
|
{"var/var 3", `x != 0; y = x; y = 1`, `y = 1; y = x; x != 0`},
|
|
{"array compr/var", `x != 0; [y | y = 1] = x`, `[y | y = 1] = x; x != 0`},
|
|
{"array compr/array", `[1] != [x]; [y | y = 1] = [x]`, `[y | y = 1] = [x]; [1] != [x]`},
|
|
{"with", `data.a.b.d.t with input as x; x = 1`, `x = 1; data.a.b.d.t with input as x`},
|
|
{"with-2", `data.a.b.d.t with input.x as x; x = 1`, `x = 1; data.a.b.d.t with input.x as x`},
|
|
{"with-nop", "data.somedoc[x] with input as true", "data.somedoc[x] with input as true"},
|
|
{"ref-head", `s = [["foo"], ["bar"]]; x = y[0]; y = s[_]; contains(x, "oo")`, `
|
|
s = [["foo"], ["bar"]];
|
|
y = s[_];
|
|
x = y[0];
|
|
contains(x, "oo")
|
|
`},
|
|
{"userfunc", `split(y, ".", z); data.a.b.funcs.fn("...foo.bar..", y)`, `data.a.b.funcs.fn("...foo.bar..", y); split(y, ".", z)`},
|
|
{"every", `every _ in [] { x != 1 }; x = 1`, `__local4__ = []; x = 1; every __local3__, _ in __local4__ { x != 1}`},
|
|
{"every-domain", `every _ in xs { true }; xs = [1]`, `xs = [1]; __local4__ = xs; every __local3__, _ in __local4__ { true }`},
|
|
{"and, implicit body", `x and y; x = true; y = false`, `x = true; y = false; x and y`},
|
|
{"or, implicit body", `x or y; x = true; y = false`, `x = true; y = false; x or y`},
|
|
{"and, explicit body", `{ x } and { y }; x = true; y = false`, `x = true; y = false; { x } and { y }`},
|
|
{"or, explicit body", `{ x } or { y }; x = true; y = false`, `x = true; y = false; { x } or { y }`},
|
|
{"and, explicit body, internal reordering", `{ 1 == z; z = 3 } and { 2 == z; z = 3 }`, `{ z = 3; equal(1, z) } and { z = 3; equal(2, z) }`},
|
|
{"or, explicit body, internal reordering", `{ 1 == z; z = 3 } or { 2 == z; z = 3 }`, `{ z = 3; equal(1, z) } or { z = 3; equal(2, z) }`},
|
|
}
|
|
|
|
for i, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
opts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
AllFutureKeywords: true,
|
|
}
|
|
c := NewCompiler()
|
|
c.Modules = getCompilerTestModules()
|
|
c.Modules["reordering"] = MustParseModuleWithOpts(fmt.Sprintf(
|
|
`package test
|
|
p if { %s }`, tc.body), opts)
|
|
|
|
compileStages(c, StageCheckSafetyRuleBodies)
|
|
|
|
if c.Failed() {
|
|
t.Errorf("%v (#%d): Unexpected compilation error: %v", tc.note, i, c.Errors)
|
|
return
|
|
}
|
|
|
|
expected := MustParseBodyWithOpts(tc.expected, opts)
|
|
result := c.Modules["reordering"].Rules[0].Body
|
|
|
|
if !expected.Equal(result) {
|
|
t.Errorf("%v (#%d): Expected body to be ordered and equal to %v but got: %v", tc.note, i, expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckSafetyBodyReorderingClosures(t *testing.T) {
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
|
|
tests := []struct {
|
|
note string
|
|
mod *Module
|
|
exp Body
|
|
}{
|
|
{
|
|
note: "comprehensions-1",
|
|
mod: MustParseModule(`package compr
|
|
import rego.v1
|
|
import data.b
|
|
import data.c
|
|
p = true if { v = [null | true]; xs = [x | a[i] = x; a = [y | y != 1; y = c[j]]]; xs[j] > 0; z = [true | data.a.b.d.t with input as i2; i2 = i]; b[i] = j }
|
|
`),
|
|
exp: MustParseBody(`v = [null | true]; data.b[i] = j; xs = [x | a = [y | y = data.c[j]; y != 1]; a[i] = x]; xs[j] > 0; z = [true | i2 = i; data.a.b.d.t with input as i2]`),
|
|
},
|
|
{
|
|
note: "comprehensions-2",
|
|
mod: MustParseModule(`package compr
|
|
import rego.v1
|
|
import data.b
|
|
import data.c
|
|
q = true if { _ = [x | x = b[i]]; _ = b[j]; _ = [x | x = true; x != false]; true != false; _ = [x | data.foo[_] = x]; data.foo[_] = _ }
|
|
`),
|
|
exp: MustParseBody(`_ = [x | x = data.b[i]]; _ = data.b[j]; _ = [x | x = true; x != false]; true != false; _ = [x | data.foo[_] = x]; data.foo[_] = _`),
|
|
},
|
|
|
|
{
|
|
note: "comprehensions-3",
|
|
mod: MustParseModule(`package compr
|
|
import rego.v1
|
|
import data.b
|
|
import data.c
|
|
fn(x) = y if {
|
|
trim(x, ".", y)
|
|
}
|
|
r = true if { a = [x | split(y, ".", z); x = z[i]; fn("...foo.bar..", y)] }
|
|
`),
|
|
exp: MustParseBody(`a = [x | data.compr.fn("...foo.bar..", y); split(y, ".", z); x = z[i]]`),
|
|
},
|
|
{
|
|
note: "closure over function output",
|
|
mod: MustParseModule(`package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
object.get(input.subject.roles[_], comp, [""], output)
|
|
comp = [ 1 | true ]
|
|
every y in [2] {
|
|
y in output
|
|
}
|
|
}`),
|
|
exp: MustParseBodyWithOpts(`comp = [1 | true]
|
|
__local2__ = [2]
|
|
object.get(input.subject.roles[_], comp, [""], output)
|
|
every __local0__, __local1__ in __local2__ { internal.member_2(__local1__, output) }`, opts),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{"mod": tc.mod}
|
|
compileStages(c, StageCheckSafetyRuleBodies)
|
|
assertNotFailed(t, c)
|
|
last := len(c.Modules["mod"].Rules) - 1
|
|
actual := c.Modules["mod"].Rules[last].Body
|
|
if !actual.Equal(tc.exp) {
|
|
t.Errorf("Expected reordered body to be equal to:\n%v\nBut got:\n%v", tc.exp, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckSafetyBodyErrors(t *testing.T) {
|
|
|
|
moduleBegin := `
|
|
package a.b
|
|
|
|
import input.aref.b.c as foo
|
|
import input.avar as bar
|
|
import data.m.n as baz
|
|
`
|
|
|
|
tests := []struct {
|
|
note string
|
|
moduleContent string
|
|
expected string
|
|
}{
|
|
{"ref-head", `p if { a.b.c = "foo" }`, `{a,}`},
|
|
{"ref-head-2", `p if { {"foo": [{"bar": a.b.c}]} = {"foo": [{"bar": "baz"}]} }`, `{a,}`},
|
|
{"negation", `p if { a = [1, 2, 3, 4]; not a[i] = x }`, `{i, x}`},
|
|
{"negation-head", `p contains x if { a = [1, 2, 3, 4]; not a[i] = x }`, `{i,x}`},
|
|
{"negation-multiple", `p if { a = [1, 2, 3, 4]; b = [1, 2, 3, 4]; not a[i] = x; not b[j] = x }`, `{i, x, j}`},
|
|
{"negation-nested", `p if { a = [{"foo": ["bar", "baz"]}]; not a[0].foo = [a[0].foo[i], a[0].foo[j]] } `, `{i, j}`},
|
|
{"builtin-input", `p if { count([1, 2, x], x) }`, `{x,}`},
|
|
{"builtin-input-name", `p if { count(eq, 1) }`, `{eq,}`},
|
|
{"builtin-multiple", `p if { x > 0; x <= 3; x != 2 }`, `{x,}`},
|
|
{"unordered-object-keys", `p if { x = "a"; [{x: y, z: a}] = [{"a": 1, "b": 2}]}`, `{a,y,z}`},
|
|
{"unordered-sets", `p if { x = "a"; [{x, y}] = [{1, 2}]}`, `{y,}`},
|
|
{"array-compr", `p if { _ = [x | x = data.a[_]; y > 1] }`, `{y,}`},
|
|
{"array-compr-nested", `p if { _ = [x | x = a[_]; a = [y | y = data.a[_]; z > 1]] }`, `{z,}`},
|
|
{"array-compr-closure", `p if { _ = [v | v = [x | x = data.a[_]]; x > 1] }`, `{x,}`},
|
|
{"array-compr-term", `p if { _ = [u | true] }`, `{u,}`},
|
|
{"array-compr-term-nested", `p if { _ = [v | v = [w | w != 0]] }`, `{w,}`},
|
|
{"array-compr-mixed", `p if { _ = [x | y = [a | a = z[i]]] }`, `{a, x, z, i}`},
|
|
{"array-compr-builtin", `p if { [true | eq != 2] }`, `{eq,}`},
|
|
{"closure-self", `p if { x = [x | x = 1] }`, `{x,}`},
|
|
{"closure-transitive", `p if { x = y; x = [y | y = 1] }`, `{x,y}`},
|
|
{"nested", `p if { count(baz[i].attr[bar[dead.beef]], n) }`, `{dead,}`},
|
|
{"negated-import", `p if { not foo; not bar; not baz }`, `set()`},
|
|
{"rewritten", `p contains {"foo": dead[i]} if { true }`, `{dead, i}`},
|
|
{"with-value", `p if { data.a.b.d.t with input as x }`, `{x,}`},
|
|
{"with-value-2", `p if { x = data.a.b.d.t with input as x }`, `{x,}`},
|
|
{"else-kw", "p if { false } else if { count(x, 1) }", `{x,}`},
|
|
{"function", "foo(x) = [y, z] if { split(x, y, z) }", `{y,z}`},
|
|
{"call-vars-input", "p if { f(x, x) } f(x) = x if { true }", `{x,}`},
|
|
{"call-no-output", "p if { f(x) } f(x) = x if { true }", `{x,}`},
|
|
{"call-too-few", "p if { f(1,x) } f(x,y) if { true }", "{x,}"},
|
|
{"object-key-comprehension", "p if { { {p|x}: 0 } }", "{x,}"},
|
|
{"set-value-comprehension", "p if { {1, {p|x}} }", "{x,}"},
|
|
{"every", "p if { every y in [10] { x > y } }", "{x,}"},
|
|
{"and, implicit body", `p if { x and y }`, "{x,y,}"},
|
|
{"or, implicit body", `p if { x or y }`, "{x,y,}"},
|
|
{"and, implicit body unification", `p if { x = 1 and y = 2 }`, "{x,y,}"},
|
|
{"or, implicit body unification", `p if { x = 1 or y = 2 }`, "{x,y,}"},
|
|
{"and, implicit body builtin call output", `p if { count(input.foo, x) and split("a/b", "/", y) }`, "{x,y,}"},
|
|
{"or, implicit body builtin call output", `p if { count(input.foo, x) or split("a/b", "/", y) }`, "{x,y,}"},
|
|
{"and, implicit body builtin call output, lhs only", `p if { count(input.foo, x) and y > 0 }`, "{x,y,}"},
|
|
{"or, implicit body builtin call output, lhs only", `p if { count(input.foo, x) or y > 0 }`, "{x,y,}"},
|
|
{"and, implicit body ref var binding", `p if { input.foo[x] and input.bar[y] }`, "{x,y,}"},
|
|
{"or, implicit body ref var binding", `p if { input.foo[x] or input.bar[y] }`, "{x,y,}"},
|
|
{"and, implicit body ref var binding, lhs only", `p if { input.foo[x] and y > 0 }`, "{x,y,}"},
|
|
{"or, implicit body ref var binding, lhs only", `p if { input.foo[x] or y > 0 }`, "{x,y,}"},
|
|
{"and, explicit body", `p if { a := true; x := true; {x; y} and {a; b} }`, "{b,y,}"},
|
|
{"or, explicit body", `p if { a := true; x := true; {x; y} or {a; b} }`, "{b,y,}"},
|
|
{"and, LHS assignment does not bind to RHS", `p if { { x := 42 } and x < 100 }`, "{x,}"},
|
|
{"or, LHS assignment does not bind ot RHS", `p if { { x := 42 } or x < 100 }`, "{x,}"},
|
|
{"and, LHS unification does not bind to RHS", `p if { { x = 42 } and x < 100 }`, "{x,}"},
|
|
{"or, LHS unification does not bind ot RHS", `p if { { x = 42 } or x < 100 }`, "{x,}"},
|
|
{"and, RHS assignment does not bind to LHS", `p if { x < 100 and { x := 42 } }`, "{x,}"},
|
|
{"or, RHS assignment does not bind to LHS", `p if { x < 100 or { x := 42 } }`, "{x,}"},
|
|
{"and, RHS unification does not bind to LHS", `p if { x < 100 and { x = 42 } }`, "{x,}"},
|
|
{"or, RHS unification does not bind to LHS", `p if { x < 100 or { x = 42 } }`, "{x,}"},
|
|
{"and, assignment does not bind to outer scope", `p if { {x := 1} and {y := 2}; x == 1; y == 2 }`, "{x,y,}"},
|
|
{"or, assignment does not bind to outer scope", `p if { {x := 1} or {y := 2}; x == 1; y == 2 }`, "{x,y,}"},
|
|
{"and, unification does not bind to outer scope", `p if { {x = 1} and {y = 2}; x == 1; y == 2 }`, "{x,y,}"},
|
|
{"or, unification does not bind to outer scope", `p if { {x = 1} or {y = 2}; x == 1; y == 2 }`, "{x,y,}"},
|
|
{"assignment RHS not made safe via LHS (issue 3546)", `p if { x := y; x = 7 }`, `{y,}`},
|
|
}
|
|
|
|
makeErrMsg := func(varName string) string {
|
|
return fmt.Sprintf("rego_unsafe_var_error: var %v is unsafe", varName)
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
|
|
// Build slice of expected error messages.
|
|
expected := []string{}
|
|
|
|
_ = MustParseTerm(tc.expected).Value.(Set).Iter(func(x *Term) error {
|
|
expected = append(expected, makeErrMsg(string(x.Value.(Var))))
|
|
return nil
|
|
}) // cannot return error
|
|
|
|
sort.Strings(expected)
|
|
|
|
// Compile test module.
|
|
opts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or"},
|
|
}
|
|
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"newMod": MustParseModuleWithOpts(fmt.Sprintf(`
|
|
|
|
%v
|
|
|
|
%v
|
|
|
|
`, moduleBegin, tc.moduleContent), opts),
|
|
}
|
|
|
|
compileStages(c, StageCheckSafetyRuleBodies)
|
|
|
|
// Get errors.
|
|
result := compilerErrsToStringSlice(c.Errors)
|
|
|
|
// Check against expected.
|
|
if len(result) != len(expected) {
|
|
t.Fatalf("Expected %d:\n%v\nBut got %d:\n%v", len(expected), strings.Join(expected, "\n"), len(result), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if expected[i] != result[i] {
|
|
t.Errorf("Expected %v but got: %v", expected[i], result[i])
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCompilerCheckSafetyAssignmentDirectional verifies that the RHS of `:=`
|
|
// cannot be made safe through the LHS. See issue #3546.
|
|
func TestCompilerCheckSafetyAssignmentDirectional(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
wantUnsafe string // unsafe var name; empty means the module must compile
|
|
}{
|
|
{
|
|
note: "RHS var never bound is unsafe",
|
|
module: `p if { x := y; x = 7 }`,
|
|
wantUnsafe: "y",
|
|
},
|
|
{
|
|
note: "RHS var only reachable by backwards unification is unsafe",
|
|
module: `p := v if { x := y; x = 7; v := obj[x] }`,
|
|
wantUnsafe: "y",
|
|
},
|
|
{
|
|
note: "RHS var bound by another expression is safe",
|
|
module: `p if { x := y; y = 7 }`,
|
|
},
|
|
{
|
|
note: "RHS ref iteration still binds its own vars",
|
|
module: `obj := {"a": 1, "b": 2}` + "\n" + `p contains v if { some k; v := obj[k] }`,
|
|
},
|
|
{
|
|
note: "chained assignments remain safe",
|
|
module: `p := y if { x := input.foo; y := x }`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"test": MustParseModule("package test\n\n" + tc.module),
|
|
}
|
|
compileStages(c, StageCheckSafetyRuleBodies)
|
|
|
|
if tc.wantUnsafe == "" {
|
|
if c.Failed() {
|
|
t.Fatalf("expected module to compile but got errors: %v", c.Errors)
|
|
}
|
|
return
|
|
}
|
|
|
|
if !c.Failed() {
|
|
t.Fatalf("expected var %q to be unsafe but module compiled", tc.wantUnsafe)
|
|
}
|
|
want := fmt.Sprintf("rego_unsafe_var_error: var %s is unsafe", tc.wantUnsafe)
|
|
found := false
|
|
for _, err := range c.Errors {
|
|
if strings.Contains(err.Error(), want) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected error %q, got: %v", want, c.Errors)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckSafetyVarLoc(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
module string
|
|
expectedErrs []struct {
|
|
message string
|
|
location int
|
|
}
|
|
}{
|
|
{
|
|
module: `package test
|
|
import rego.v1
|
|
|
|
p if {
|
|
not x
|
|
x > y
|
|
}`,
|
|
expectedErrs: []struct {
|
|
message string
|
|
location int
|
|
}{
|
|
{
|
|
"var x is unsafe",
|
|
5,
|
|
},
|
|
{
|
|
"var y is unsafe",
|
|
6,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
module: `package play
|
|
|
|
obj := {
|
|
"foo": "bar",
|
|
"baz": qux,
|
|
}
|
|
`,
|
|
expectedErrs: []struct {
|
|
message string
|
|
location int
|
|
}{
|
|
{
|
|
"var qux is unsafe",
|
|
5,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// Rewritten head vars, e.g. from `some`, must still report a location.
|
|
module: `package test
|
|
|
|
r[x] := y if {
|
|
some x
|
|
y := [1]
|
|
}`,
|
|
expectedErrs: []struct {
|
|
message string
|
|
location int
|
|
}{
|
|
{
|
|
"var x is unsafe",
|
|
3,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.module, func(t *testing.T) {
|
|
_, err := CompileModules(map[string]string{"test.rego": tc.module})
|
|
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
var errs Errors
|
|
errors.As(err, &errs)
|
|
|
|
if len(errs) != len(tc.expectedErrs) {
|
|
t.Fatalf("expected %d errors, got %d", len(tc.expectedErrs), len(errs))
|
|
}
|
|
|
|
for i := range errs {
|
|
if !strings.Contains(errs[i].Message, tc.expectedErrs[i].message) || errs[i].Location.Row != tc.expectedErrs[i].location {
|
|
t.Fatalf("expected error on row %d but got: %s", tc.expectedErrs[i].location, errs[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerCheckSafetyFunctionAndContainsKeyword(t *testing.T) {
|
|
_, err := CompileModules(map[string]string{"test.rego": `package play
|
|
|
|
import future.keywords.contains
|
|
|
|
p(id) contains x {
|
|
x := id
|
|
}`})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
|
|
errs := err.(Errors)
|
|
if !strings.Contains(errs[0].Message, "the contains keyword can only be used with multi-value rule definitions (e.g., p contains <VALUE> { ... })") {
|
|
t.Fatal("wrong error message:", err)
|
|
}
|
|
if errs[0].Location.Row != 5 {
|
|
t.Fatal("expected error on line 5 but got:", errs[0].Location.Row)
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckTypes(t *testing.T) {
|
|
c := NewCompiler()
|
|
modules := getCompilerTestModules()
|
|
c.Modules = map[string]*Module{"mod6": modules["mod6"], "mod7": modules["mod7"]}
|
|
compileStages(c, StageCheckTypes)
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
// Regression test for GH issue #6790
|
|
func TestCompilerCheckEveryWithNestedDomainCalls(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{"test": MustParseModule(`package test
|
|
import rego.v1
|
|
|
|
x if {
|
|
every p in [1 / 2] {
|
|
p == true
|
|
}
|
|
}`)}
|
|
compileStages(c, StageCheckTypes)
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
func TestCompilerCheckRuleConflicts(t *testing.T) {
|
|
|
|
c := getCompilerWithParsedModules(map[string]string{
|
|
"mod1.rego": `package badrules
|
|
|
|
p contains x if { x = 1 }
|
|
p[x] = y if { x = y; x = "a" }
|
|
q contains 1 if { true }
|
|
q = {1, 2, 3} if { true }
|
|
r[x] = y if { x = y; x = "a" }
|
|
r[x] = y if { x = y; x = "a" }
|
|
s contains x if { x = "a" }
|
|
s contains x if { x = "b" }
|
|
t := x if { x = "a"}`,
|
|
|
|
// valid extension of r in mod1.rego
|
|
"mod2a.rego": `package badrules.r
|
|
|
|
q contains 1 if { true }`,
|
|
|
|
// invalid override of s in mod1.rego
|
|
"mod2b.rego": `package badrules.s
|
|
|
|
q contains 1 if { true }`,
|
|
|
|
// invalid override of t in mod1.rego
|
|
"mod2c.rego": `package badrules.t
|
|
|
|
q contains 1 if { true }`,
|
|
|
|
"mod3.rego": `package badrules.defkw
|
|
|
|
default foo = 1
|
|
default foo = 2
|
|
foo = 3 if { true }
|
|
|
|
default p.q.bar = 1
|
|
default p.q.bar = 2
|
|
p.q.bar = 3 if { true }
|
|
`,
|
|
"mod4.rego": `package badrules.arity
|
|
|
|
f(1) if { true }
|
|
f if { true }
|
|
|
|
g(1) if { true }
|
|
g(1,2) if { true }
|
|
|
|
p.q.h(1) if { true }
|
|
p.q.h if { true }
|
|
|
|
p.q.i(1) if { true }
|
|
p.q.i(1,2) if { true }`,
|
|
"mod5.rego": `package badrules.dataoverlap
|
|
|
|
p if { true }`,
|
|
"mod6.rego": `package badrules.existserr
|
|
|
|
p if { true }`,
|
|
|
|
"mod7.rego": `package badrules.foo
|
|
|
|
bar.baz contains "quz" if true`,
|
|
"mod8.rego": `package badrules.complete_partial
|
|
p := 1
|
|
p[r] := 2 if { r := "foo" }`,
|
|
})
|
|
|
|
c.WithPathConflictsCheck(func(path []string) (bool, error) {
|
|
if slices.Equal(path, []string{"badrules", "dataoverlap", "p"}) {
|
|
return true, nil
|
|
} else if slices.Equal(path, []string{"badrules", "existserr", "p"}) {
|
|
return false, errors.New("unexpected error")
|
|
}
|
|
return false, nil
|
|
})
|
|
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
|
|
expected := []string{
|
|
"rego_compile_error: conflict check for data path badrules/existserr/p: unexpected error",
|
|
"rego_compile_error: conflicting rule for data path badrules/dataoverlap/p found",
|
|
"rego_type_error: conflicting rules data.badrules.arity.f found",
|
|
"rego_type_error: conflicting rules data.badrules.arity.g found",
|
|
"rego_type_error: conflicting rules data.badrules.arity.p.q.h found",
|
|
"rego_type_error: conflicting rules data.badrules.arity.p.q.i found",
|
|
"rego_type_error: conflicting rules data.badrules.complete_partial.p[r] found",
|
|
"rego_type_error: conflicting rules data.badrules.p[x] found",
|
|
"rego_type_error: conflicting rules data.badrules.q found",
|
|
"rego_type_error: multiple default rules data.badrules.defkw.foo found at mod3.rego:3, mod3.rego:4",
|
|
"rego_type_error: multiple default rules data.badrules.defkw.p.q.bar found at mod3.rego:7, mod3.rego:8",
|
|
"rego_type_error: package badrules.s conflicts with rule s defined at mod1.rego:10",
|
|
"rego_type_error: package badrules.s conflicts with rule s defined at mod1.rego:9",
|
|
"rego_type_error: package badrules.t conflicts with rule t defined at mod1.rego:11",
|
|
"rego_type_error: rule data.badrules.s conflicts with:\n rule data.badrules.s.q at mod2b.rego:3",
|
|
"rego_type_error: rule data.badrules.t conflicts with:\n rule data.badrules.t.q at mod2c.rego:3",
|
|
}
|
|
|
|
assertCompilerErrorStrings(t, c, expected)
|
|
}
|
|
|
|
func TestCompilerCheckRuleConflictsWithRoots(t *testing.T) {
|
|
|
|
c := getCompilerWithParsedModules(map[string]string{
|
|
"mod1.rego": `package badrules.dataoverlap
|
|
p if { true }`,
|
|
"mod2.rego": `package badrules.existserr
|
|
p if { true }`,
|
|
|
|
// this does not trigger conflict check because
|
|
// WithPathConflictsCheckRoots limits the root to "badrules".
|
|
"mod3.rego": `package badrules_outside_root.dataoverlap
|
|
p if { true }`,
|
|
})
|
|
|
|
c.WithPathConflictsCheck(func(path []string) (bool, error) {
|
|
if slices.Contains(path, "dataoverlap") {
|
|
return true, nil
|
|
} else if slices.Equal(path, []string{"badrules", "existserr", "p"}) {
|
|
return false, errors.New("unexpected error")
|
|
}
|
|
return false, nil
|
|
}).WithPathConflictsCheckRoots([]string{"badrules"})
|
|
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
|
|
expected := []string{
|
|
"rego_compile_error: conflict check for data path badrules/existserr/p: unexpected error",
|
|
"rego_compile_error: conflicting rule for data path badrules/dataoverlap/p found",
|
|
}
|
|
|
|
assertCompilerErrorStrings(t, c, expected)
|
|
}
|
|
|
|
func TestCompilerCheckRuleConflictsDefaultFunction(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
err string
|
|
}{
|
|
{
|
|
note: "conflicting rules",
|
|
modules: modules(
|
|
`package pkg
|
|
default f(_) = 100
|
|
f(x, y) = x if {
|
|
x == y
|
|
}`),
|
|
err: "rego_type_error: conflicting rules data.pkg.f found",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
mods := make(map[string]*Module, len(tc.modules))
|
|
for i, m := range tc.modules {
|
|
mods[strconv.Itoa(i)] = m
|
|
}
|
|
c := NewCompiler()
|
|
c.Modules = mods
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
if tc.err != "" {
|
|
assertCompilerErrorStrings(t, c, []string{tc.err})
|
|
} else {
|
|
assertCompilerErrorStrings(t, c, []string{})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckRuleConflictsDotsInRuleHeads(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
err string
|
|
}{
|
|
{
|
|
note: "arity mismatch, ref and non-ref rule",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.r if { true }`,
|
|
`package pkg.p.q
|
|
r(_) = 2`),
|
|
err: "rego_type_error: conflicting rules data.pkg.p.q.r found",
|
|
},
|
|
{
|
|
note: "two default rules, ref and non-ref rule",
|
|
modules: modules(
|
|
`package pkg
|
|
default p.q.r = 3
|
|
p.q.r if { true }`,
|
|
`package pkg.p.q
|
|
default r = 4
|
|
r = 2`),
|
|
err: "rego_type_error: multiple default rules data.pkg.p.q.r found at mod0.rego:2, mod1.rego:2",
|
|
},
|
|
{
|
|
note: "arity mismatch, ref and ref rule",
|
|
modules: modules(
|
|
`package pkg.a.b
|
|
p.q.r if { true }`,
|
|
`package pkg.a
|
|
b.p.q.r(_) = 2`),
|
|
err: "rego_type_error: conflicting rules data.pkg.a.b.p.q.r found",
|
|
},
|
|
{
|
|
note: "two default rules, ref and ref rule",
|
|
modules: modules(
|
|
`package pkg
|
|
default p.q.w.r = 3
|
|
p.q.w.r if { true }`,
|
|
`package pkg.p
|
|
default q.w.r = 4
|
|
q.w.r = 2`),
|
|
err: "rego_type_error: multiple default rules data.pkg.p.q.w.r found at mod0.rego:2, mod1.rego:2",
|
|
},
|
|
{
|
|
note: "multi-value + single-value rules, both with same ref prefix",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.w[x] = 1 if x := "foo"`,
|
|
`package pkg
|
|
p.q.w contains "bar"`),
|
|
err: "rego_type_error: conflicting rules data.pkg.p.q.w found",
|
|
},
|
|
{
|
|
note: "two multi-value rules, both with same ref",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.w contains "baz"`,
|
|
`package pkg
|
|
p.q.w contains "bar"`),
|
|
},
|
|
{
|
|
note: "module conflict: non-ref rule",
|
|
modules: modules(
|
|
`package pkg.q
|
|
r if { true }`,
|
|
`package pkg.q.r`),
|
|
err: "rego_type_error: package pkg.q.r conflicts with rule r defined at mod0.rego:2",
|
|
},
|
|
{
|
|
note: "module conflict: ref rule",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.r if { true }`,
|
|
`package pkg.p.q.r`),
|
|
err: "rego_type_error: package pkg.p.q.r conflicts with rule p.q.r defined at mod0.rego:2",
|
|
},
|
|
{
|
|
note: "single-value with other rule overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.r if { true }`,
|
|
`package pkg
|
|
p.q.r.s if { true }`),
|
|
err: "rego_type_error: rule data.pkg.p.q.r conflicts with:\n rule data.pkg.p.q.r.s at mod1.rego:2",
|
|
},
|
|
{
|
|
note: "single-value with other rule overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q.r if { true }
|
|
p.q.r.s if { true }
|
|
p.q.r.t if { true }`),
|
|
err: "rego_type_error: rule data.pkg.p.q.r conflicts with:\n rule data.pkg.p.q.r.s at mod0.rego:3\n rule data.pkg.p.q.r.t at mod0.rego:4",
|
|
},
|
|
{
|
|
note: "single-value with other rule overlap, unknown key",
|
|
modules: modules(
|
|
`package pkg
|
|
p := x if { x := 1 }
|
|
p.r[s] := 2 if { s := "foo" }`),
|
|
err: "rego_type_error: rule data.pkg.p conflicts with:\n rule data.pkg.p.r[s] at mod0.rego:3",
|
|
},
|
|
{
|
|
note: "single-value with other partial object (same ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q := 1
|
|
p.q[r] := 2 if { r := "foo" }`),
|
|
err: "rego_type_error: conflicting rules data.pkg.p.q[r] foun",
|
|
},
|
|
{
|
|
note: "single-value with other rule overlap, unknown key",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q[r] = x if { r = input.key; x = input.foo }
|
|
p.q.r.s = x if { true }
|
|
`),
|
|
},
|
|
{
|
|
note: "single-value with other rule overlap, unknown ref var and key",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q[r][s] = x if { r = input.key1; s = input.key2; x = input.foo }
|
|
p.q.r.s.t = x if { true }
|
|
`),
|
|
},
|
|
{
|
|
note: "single-value partial object with other partial object rule overlap, unknown keys (regression test for #5855; invalidated by multi-var refs)",
|
|
modules: modules(
|
|
`package pkg
|
|
p[r] := x if { r = input.key; x = input.bar }
|
|
p.q[r] := x if { r = input.key; x = input.bar }
|
|
`),
|
|
},
|
|
{
|
|
note: "single-value partial object with other partial object (implicit 'true' value) rule overlap, unknown keys",
|
|
modules: modules(
|
|
`package pkg
|
|
p[r] := x if { r = input.key; x = input.bar }
|
|
p.q[r] if { r = input.key }
|
|
`),
|
|
},
|
|
{
|
|
note: "single-value partial object with multi-value rule (ref head) overlap, unknown key",
|
|
modules: modules(
|
|
`package pkg
|
|
import future.keywords
|
|
p[r] := x if { r = input.key; x = input.bar }
|
|
p.q contains r if { r = input.key }
|
|
`),
|
|
},
|
|
{
|
|
note: "single-value partial object with multi-value rule overlap, unknown key",
|
|
modules: modules(
|
|
`package pkg
|
|
p[r] := x if { r = input.key; x = input.bar }
|
|
p contains q if { true }
|
|
`),
|
|
err: "rego_type_error: conflicting rules data.pkg.p found",
|
|
},
|
|
{
|
|
note: "single-value rule with known and unknown key",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q[r] = x if { r = input.key; x = input.foo }
|
|
p.q.s = "x" if { true }
|
|
`),
|
|
},
|
|
{
|
|
note: "multi-value rule with other rule overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p contains v if { v := ["a", "b"][_] }
|
|
p.q := 42
|
|
`),
|
|
err: "rego_type_error: rule data.pkg.p conflicts with:\n rule data.pkg.p.q at mod0.rego:3",
|
|
},
|
|
{
|
|
note: "multi-value rule with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p contains v if { v := ["a", "b"][_] }
|
|
p.q.r if { true }
|
|
`),
|
|
err: "rego_type_error: rule data.pkg.p conflicts with:\n rule data.pkg.p.q.r at mod0.rego:3",
|
|
},
|
|
{
|
|
note: "multi-value rule (dots in head) with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
import future.keywords
|
|
p.q contains v if { v := ["a", "b"][_] }
|
|
p.q.r if { true }
|
|
`),
|
|
err: "rule data.pkg.p.q conflicts with:\n rule data.pkg.p.q.r at mod0.rego:4",
|
|
},
|
|
{
|
|
note: "multi-value rule (dots and var in head) with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
import future.keywords
|
|
p[q] contains v if { v := ["a", "b"][_] }
|
|
p.q.r if { true }
|
|
`),
|
|
},
|
|
{
|
|
note: "multi-value set rule with multi-value general-ref (object) rule, same name (regression test for #8860)",
|
|
modules: modules(
|
|
`package pkg
|
|
import future.keywords
|
|
index contains p.id if { some p in input.prefs }
|
|
index[p.id] contains p if { some p in input.prefs }
|
|
`),
|
|
err: "rego_type_error: conflicting rules data.pkg.index[",
|
|
},
|
|
{
|
|
note: "multi-value general-ref (object) rules with same name do not conflict",
|
|
modules: modules(
|
|
`package pkg
|
|
import future.keywords
|
|
index[k] contains v if { k := "a"; v := 1 }
|
|
index[k] contains v if { k := "b"; v := 2 }
|
|
`),
|
|
},
|
|
{
|
|
note: "function with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p(x) := x
|
|
p.q.r if { true }
|
|
`),
|
|
err: "rego_type_error: rule data.pkg.p conflicts with:\n rule data.pkg.p.q.r at mod0.rego:3",
|
|
},
|
|
{
|
|
note: "function with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p(x) := x
|
|
p.q.r if { true }
|
|
`),
|
|
err: "rego_type_error: rule data.pkg.p conflicts with:\n rule data.pkg.p.q.r at mod0.rego:3",
|
|
},
|
|
{
|
|
note: "function (ref) with other rule (ref) overlap",
|
|
modules: modules(
|
|
`package pkg
|
|
p.q(x) := x
|
|
p.q.r if { true }
|
|
`),
|
|
err: "rego_type_error: rule data.pkg.p.q conflicts with:\n rule data.pkg.p.q.r at mod0.rego:3",
|
|
},
|
|
{
|
|
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:\n rule data.pkg.a.b.d at mod0.rego:3",
|
|
},
|
|
{
|
|
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:\n rule data.pkg.a.b.d at mod1.rego:2",
|
|
},
|
|
{
|
|
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:\n rule data.pkg.p.r.s.t at mod0.rego:3",
|
|
},
|
|
{
|
|
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) {
|
|
mods := make(map[string]*Module, len(tc.modules))
|
|
for i, m := range tc.modules {
|
|
mods[strconv.Itoa(i)] = m
|
|
}
|
|
c := NewCompiler()
|
|
c.Modules = mods
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
if tc.err != "" {
|
|
assertCompilerErrorStrings(t, c, []string{tc.err})
|
|
} else {
|
|
assertCompilerErrorStrings(t, c, []string{})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckRulePkgConflicts(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules []*Module
|
|
err []string
|
|
}{
|
|
{
|
|
note: "Package can be declared within dynamic extent of rule (#6387 regression test)",
|
|
modules: modules(
|
|
`package test
|
|
p[x] := y if { x := "a"; y := "b" }`,
|
|
`package test.p
|
|
q := 1`),
|
|
},
|
|
{
|
|
note: "Package can be declared deep within dynamic extent of rule (#6387 regression test)",
|
|
modules: modules(
|
|
`package test
|
|
p[x] := y if { x := "a"; y := "b" }`,
|
|
`package test.p.q.r.s
|
|
t := 1`),
|
|
},
|
|
{
|
|
note: "Package cannot be declared within extent of single-value rule (ground ref)",
|
|
modules: modules(
|
|
`package test
|
|
p := x if { x := "a" }`,
|
|
`package test.p
|
|
q := 1`),
|
|
err: []string{
|
|
"rego_type_error: package test.p conflicts with rule p defined at mod0.rego:2",
|
|
"rego_type_error: rule data.test.p conflicts with:\n rule data.test.p.q at mod1.rego:2",
|
|
},
|
|
},
|
|
{
|
|
note: "Package cannot be declared within extent of multi-value rule",
|
|
modules: modules(
|
|
`package test
|
|
p contains x if { x := "a" }`,
|
|
`package test.p
|
|
q := 1`),
|
|
err: []string{
|
|
"rego_type_error: package test.p conflicts with rule p defined at mod0.rego:2",
|
|
"rego_type_error: rule data.test.p conflicts with:\n rule data.test.p.q at mod1.rego:2",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
mods := make(map[string]*Module, len(tc.modules))
|
|
for i, m := range tc.modules {
|
|
mods[strconv.Itoa(i)] = m
|
|
}
|
|
c := NewCompiler()
|
|
c.Modules = mods
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
if len(tc.err) > 0 {
|
|
assertCompilerErrorStrings(t, c, tc.err)
|
|
} else {
|
|
assertCompilerErrorStrings(t, c, []string{})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckUndefinedFuncs(t *testing.T) {
|
|
|
|
module := `
|
|
package test
|
|
import rego.v1
|
|
|
|
undefined_function if {
|
|
data.deadbeef(x)
|
|
}
|
|
|
|
undefined_global if {
|
|
deadbeef(x)
|
|
}
|
|
|
|
# NOTE: all the dynamic dispatch examples here are not supported,
|
|
# we're checking assertions about the error returned.
|
|
undefined_dynamic_dispatch if {
|
|
x = "f"; data.test2[x](1)
|
|
}
|
|
|
|
undefined_dynamic_dispatch_declared_var if {
|
|
y := "f"; data.test2[y](1)
|
|
}
|
|
|
|
undefined_dynamic_dispatch_declared_var_in_array if {
|
|
z := "f"; data.test2[[z]](1)
|
|
}
|
|
|
|
arity_mismatch_1 if {
|
|
data.test2.f(1,2,3)
|
|
}
|
|
|
|
arity_mismatch_2 if {
|
|
data.test2.f()
|
|
}
|
|
|
|
arity_mismatch_3 if {
|
|
x:= data.test2.f()
|
|
}
|
|
`
|
|
|
|
module2 := `
|
|
package test2
|
|
|
|
f(x) = x
|
|
`
|
|
|
|
_, err := CompileModules(map[string]string{
|
|
"test.rego": module,
|
|
"test2.rego": module2,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected errors")
|
|
}
|
|
|
|
result := err.Error()
|
|
want := []string{
|
|
"rego_type_error: undefined function data.deadbeef",
|
|
"rego_type_error: undefined function deadbeef",
|
|
"rego_type_error: undefined function data.test2[x]",
|
|
"rego_type_error: undefined function data.test2[y]",
|
|
"rego_type_error: undefined function data.test2[[z]]",
|
|
"rego_type_error: function data.test2.f has arity 1, got 3 arguments",
|
|
"test.rego:32: rego_type_error: function data.test2.f has arity 1, got 0 arguments",
|
|
"test.rego:36: rego_type_error: function data.test2.f has arity 1, got 0 arguments",
|
|
}
|
|
for _, w := range want {
|
|
if !strings.Contains(result, w) {
|
|
t.Fatalf("Expected %q in result but got: %v", w, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompilerQueryCompilerCheckUndefinedFuncs(t *testing.T) {
|
|
compiler := NewCompiler()
|
|
|
|
for _, tc := range []struct {
|
|
note, query, err string
|
|
}{
|
|
|
|
{note: "undefined function", query: `data.foo(1)`, err: "undefined function data.foo"},
|
|
{note: "undefined global function", query: `foo(1)`, err: "undefined function foo"},
|
|
{note: "var", query: `x = "f"; data[x](1)`, err: "undefined function data[x]"},
|
|
{note: "declared var", query: `x := "f"; data[x](1)`, err: "undefined function data[x]"},
|
|
{note: "declared var in array", query: `x := "f"; data[[x]](1)`, err: "undefined function data[[x]]"},
|
|
} {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
_, err := compiler.QueryCompiler().Compile(MustParseBody(tc.query))
|
|
if !strings.Contains(err.Error(), tc.err) {
|
|
t.Errorf("Unexpected compilation error: %v (want %s)", err, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerImportsResolved(t *testing.T) {
|
|
|
|
modules := map[string]*Module{
|
|
"mod1": MustParseModule(`package ex
|
|
|
|
import data
|
|
import input
|
|
import data.foo
|
|
import input.bar
|
|
import data.abc as baz
|
|
import input.abc as qux`,
|
|
),
|
|
}
|
|
|
|
c := NewCompiler()
|
|
c.Compile(modules)
|
|
|
|
assertNotFailed(t, c)
|
|
|
|
if len(c.Modules["mod1"].Imports) != 0 {
|
|
t.Fatalf("Expected imports to be empty after compile but got: %v", c.Modules)
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerExprExpansion(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
note string
|
|
input string
|
|
expected []*Expr
|
|
}{
|
|
{
|
|
note: "identity",
|
|
input: "x",
|
|
expected: []*Expr{
|
|
MustParseExpr("x"),
|
|
},
|
|
},
|
|
{
|
|
note: "single",
|
|
input: "x+y",
|
|
expected: []*Expr{
|
|
MustParseExpr("x+y"),
|
|
},
|
|
},
|
|
{
|
|
note: "chained",
|
|
input: "x+y+z+w",
|
|
expected: []*Expr{
|
|
MustParseExpr("plus(x, y, __local0__)"),
|
|
MustParseExpr("plus(__local0__, z, __local1__)"),
|
|
MustParseExpr("plus(__local1__, w)"),
|
|
},
|
|
},
|
|
{
|
|
note: "assoc",
|
|
input: "x+y*z",
|
|
expected: []*Expr{
|
|
MustParseExpr("mul(y, z, __local0__)"),
|
|
MustParseExpr("plus(x, __local0__)"),
|
|
},
|
|
},
|
|
{
|
|
note: "refs",
|
|
input: "p[q[f(x)]][g(x)]",
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__)"),
|
|
MustParseExpr("g(x, __local1__)"),
|
|
MustParseExpr("p[q[__local0__]][__local1__]"),
|
|
},
|
|
},
|
|
{
|
|
note: "arrays",
|
|
input: "[[f(x)], g(x)]",
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__)"),
|
|
MustParseExpr("g(x, __local1__)"),
|
|
MustParseExpr("[[__local0__], __local1__]"),
|
|
},
|
|
},
|
|
{
|
|
note: "objects",
|
|
input: `{f(x): {g(x): h(x)}}`,
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__)"),
|
|
MustParseExpr("g(x, __local1__)"),
|
|
MustParseExpr("h(x, __local2__)"),
|
|
MustParseExpr("{__local0__: {__local1__: __local2__}}"),
|
|
},
|
|
},
|
|
{
|
|
note: "sets",
|
|
input: `{f(x), {g(x)}}`,
|
|
expected: []*Expr{
|
|
MustParseExpr("g(x, __local0__)"),
|
|
MustParseExpr("f(x, __local1__)"),
|
|
MustParseExpr("{__local1__, {__local0__,}}"),
|
|
},
|
|
},
|
|
{
|
|
note: "unify",
|
|
input: "f(x) = g(x)",
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__)"),
|
|
MustParseExpr("g(x, __local1__)"),
|
|
MustParseExpr("__local0__ = __local1__"),
|
|
},
|
|
},
|
|
{
|
|
note: "unify: composites",
|
|
input: "[x, f(x)] = [g(y), y]",
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__)"),
|
|
MustParseExpr("g(y, __local1__)"),
|
|
MustParseExpr("[x, __local0__] = [__local1__, y]"),
|
|
},
|
|
},
|
|
{
|
|
note: "with: term expr",
|
|
input: "f[x+1] with input as q",
|
|
expected: []*Expr{
|
|
MustParseExpr("plus(x, 1, __local0__) with input as q"),
|
|
MustParseExpr("f[__local0__] with input as q"),
|
|
},
|
|
},
|
|
{
|
|
note: "with: call expr",
|
|
input: `f(x) = g(x) with input as p`,
|
|
expected: []*Expr{
|
|
MustParseExpr("f(x, __local0__) with input as p"),
|
|
MustParseExpr("g(x, __local1__) with input as p"),
|
|
MustParseExpr("__local0__ = __local1__ with input as p"),
|
|
},
|
|
},
|
|
{
|
|
note: "comprehensions",
|
|
input: `f(y) = [[plus(x,1) | x = sum(y[z+1])], g(w)]`,
|
|
expected: []*Expr{
|
|
MustParseExpr("f(y, __local0__)"),
|
|
MustParseExpr("g(w, __local4__)"),
|
|
MustParseExpr("__local0__ = [[__local1__ | plus(z,1,__local2__); sum(y[__local2__], __local3__); eq(x, __local3__); plus(x, 1, __local1__)], __local4__]"),
|
|
},
|
|
},
|
|
{
|
|
note: "indirect references",
|
|
input: `[1, 2, 3][i]`,
|
|
expected: []*Expr{
|
|
MustParseExpr("__local0__ = [1, 2, 3]"),
|
|
MustParseExpr("__local0__[i]"),
|
|
},
|
|
},
|
|
{
|
|
note: "multiple indirect references",
|
|
input: `split(split("foo.bar:qux", ".")[_], ":")[i]`,
|
|
expected: []*Expr{
|
|
MustParseExpr(`split("foo.bar:qux", ".", __local0__)`),
|
|
MustParseExpr(`split(__local0__[_], ":", __local1__)`),
|
|
MustParseExpr(`__local1__[i]`),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
gen := newLocalVarGenerator("", NullTerm())
|
|
expr := MustParseExpr(tc.input)
|
|
result := expandExpr(gen, expr.Copy())
|
|
if len(result) != len(tc.expected) {
|
|
t.Fatalf("Expected %v exprs but got %v:\n\nExpected:\n\n%v\n\nGot:\n\n%v", len(tc.expected), len(result), Body(tc.expected), Body(result))
|
|
}
|
|
for i := range tc.expected {
|
|
if !tc.expected[i].Equal(result[i]) {
|
|
t.Fatalf("Expected expr %d to be %v but got: %v\n\nExpected:\n\n%v\n\nGot:\n\n%v", i, tc.expected[i], result[i], Body(tc.expected), Body(result))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteExprTerms(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
expected any
|
|
}{
|
|
{
|
|
note: "base",
|
|
module: `
|
|
package test
|
|
|
|
p { x = a + b * y }
|
|
|
|
q[[data.test.f(x)]] { x = 1 }
|
|
|
|
r = [data.test.f(x)] { x = 1 }
|
|
|
|
f(x) = data.test.g(x)
|
|
|
|
pi = 3 + .14
|
|
|
|
with_value { 1 with input as f(1) }
|
|
`,
|
|
expected: `
|
|
package test
|
|
|
|
p { mul(b, y, __local1__); plus(a, __local1__, __local2__); eq(x, __local2__) }
|
|
|
|
q[[__local3__]] { x = 1; data.test.f(x, __local3__) }
|
|
|
|
r = [__local4__] { x = 1; data.test.f(x, __local4__) }
|
|
|
|
f(__local0__) = __local5__ { data.test.g(__local0__, __local5__) }
|
|
|
|
pi = __local6__ { plus(3, 0.14, __local6__) }
|
|
|
|
with_value { data.test.f(1, __local7__); 1 with input as __local7__ }
|
|
`,
|
|
},
|
|
{
|
|
note: "builtin calls in head",
|
|
module: `
|
|
package test
|
|
|
|
f(1+1) = 7
|
|
`,
|
|
expected: Errors{&Error{Message: "rule arguments cannot contain calls"}},
|
|
},
|
|
{
|
|
note: "builtin calls in head",
|
|
module: `
|
|
package test
|
|
|
|
f(object.get(x)) { object := {"a": 1}; object.a == x }
|
|
`,
|
|
expected: Errors{&Error{Message: "rule arguments cannot contain calls"}},
|
|
},
|
|
{
|
|
note: "indirect ref in args",
|
|
module: `
|
|
package test
|
|
|
|
f([1][0]) { true }`,
|
|
expected: `
|
|
package test
|
|
|
|
f(__local0__[0]) { __local0__ = [1] }`,
|
|
},
|
|
{
|
|
note: "every: domain (array)",
|
|
module: `
|
|
package test
|
|
|
|
p { every x in [1,2] { x } }`,
|
|
expected: `
|
|
package test
|
|
|
|
p { __local2__ = [1, 2]; every __local0__, __local1__ in __local2__ { __local1__ } }`,
|
|
},
|
|
{
|
|
note: "every: domain (call)",
|
|
module: `
|
|
package test
|
|
|
|
p { every x in numbers.range(1, 3) { x } }`,
|
|
expected: `
|
|
package test
|
|
|
|
p = true {
|
|
numbers.range(1, 3, __local3__)
|
|
__local2__ = __local3__
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
note: "every: domain (nested calls)",
|
|
module: `
|
|
package test
|
|
|
|
p { every x in numbers.range(1 + 2, 3 * 4) { x } }`,
|
|
expected: `
|
|
package test
|
|
|
|
p = true {
|
|
plus(1, 2, __local3__)
|
|
mul(3, 4, __local4__)
|
|
numbers.range(__local3__, __local4__, __local5__)
|
|
__local2__ = __local5__
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__
|
|
}
|
|
}`,
|
|
},
|
|
// Regression test for GH issue #6790
|
|
{
|
|
note: "every: domain (array with call)",
|
|
module: `
|
|
package test
|
|
|
|
p { every x in [1 / 2, "foo", abs(-1)] { x } }`,
|
|
expected: `
|
|
package test
|
|
|
|
p = true {
|
|
div(1, 2, __local3__)
|
|
abs(-1, __local4__)
|
|
__local2__ = [__local3__, "foo", __local4__]
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
note: "every: domain (nested array with call)",
|
|
module: `
|
|
package test
|
|
|
|
p { every x in [1 / 2, ["foo", abs(-1)]] { x } }`,
|
|
expected: `
|
|
package test
|
|
|
|
p = true {
|
|
div(1, 2, __local3__)
|
|
abs(-1, __local4__)
|
|
__local2__ = [__local3__, ["foo", __local4__]]
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__
|
|
}
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler()
|
|
opts := ParserOptions{
|
|
RegoVersion: RegoV0,
|
|
AllFutureKeywords: true,
|
|
}
|
|
|
|
compiler.Modules = map[string]*Module{
|
|
"test": MustParseModuleWithOpts(tc.module, opts),
|
|
}
|
|
compileStages(compiler, StageRewriteExprTerms)
|
|
|
|
switch exp := tc.expected.(type) {
|
|
case string:
|
|
assertNotFailed(t, compiler)
|
|
|
|
expected := MustParseModuleWithOpts(exp, opts)
|
|
|
|
if !expected.Equal(compiler.Modules["test"]) {
|
|
t.Fatalf("Expected modules to be equal. Expected:\n\n%v\n\nGot:\n\n%v", expected, compiler.Modules["test"])
|
|
}
|
|
case Errors:
|
|
assertErrors(t, compiler.Errors, exp, false)
|
|
default:
|
|
t.Fatalf("Unsupported value type for test case 'expected' field: %v", exp)
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIllegalFunctionCallRewrite(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
expectedErrors []string
|
|
}{
|
|
/*{
|
|
note: "function call override in function value",
|
|
module: `package test
|
|
foo(x) := x
|
|
|
|
p := foo(bar) {
|
|
#foo := 1
|
|
bar := 2
|
|
}`,
|
|
expectedErrors: []string{
|
|
"undefined function foo",
|
|
},
|
|
},*/
|
|
{
|
|
note: "function call override in array comprehension value",
|
|
module: `package test
|
|
p := [foo(bar) | foo := 1; bar := 2]`,
|
|
expectedErrors: []string{
|
|
"called function foo shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "function call override in set comprehension value",
|
|
module: `package test
|
|
p := {foo(bar) | foo := 1; bar := 2}`,
|
|
expectedErrors: []string{
|
|
"called function foo shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "function call override in object comprehension value",
|
|
module: `package test
|
|
p := {foo(bar): bar(foo) | foo := 1; bar := 2}`,
|
|
expectedErrors: []string{
|
|
"called function bar shadowed",
|
|
"called function foo shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "function call override in array comprehension value",
|
|
module: `package test
|
|
p := [foo.bar(baz) | foo := 1; bar := 2; baz := 3]`,
|
|
expectedErrors: []string{
|
|
"called function foo.bar shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "nested function call override in array comprehension value",
|
|
module: `package test
|
|
p := [baz(foo(bar)) | foo := 1; bar := 2]`,
|
|
expectedErrors: []string{
|
|
"called function foo shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "function call override of 'input' root document",
|
|
module: `package test
|
|
p := [input() | input := 1]`,
|
|
expectedErrors: []string{
|
|
"called function input shadowed",
|
|
},
|
|
},
|
|
{
|
|
note: "function call override of 'data' root document",
|
|
module: `package test
|
|
p := [data() | data := 1]`,
|
|
expectedErrors: []string{
|
|
"called function data shadowed",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler()
|
|
opts := ParserOptions{
|
|
RegoVersion: RegoV0,
|
|
AllFutureKeywords: true,
|
|
}
|
|
|
|
compiler.Modules = map[string]*Module{
|
|
"test": MustParseModuleWithOpts(tc.module, opts),
|
|
}
|
|
compileStages(compiler, StageRewriteLocalVars)
|
|
|
|
result := make([]string, 0, len(compiler.Errors))
|
|
for i := range compiler.Errors {
|
|
result = append(result, compiler.Errors[i].Message)
|
|
}
|
|
|
|
sort.Strings(tc.expectedErrors)
|
|
sort.Strings(result)
|
|
|
|
if len(tc.expectedErrors) != len(result) {
|
|
t.Fatalf("Expected %d errors but got %d:\n\n%v\n\nGot:\n\n%v",
|
|
len(tc.expectedErrors), len(result),
|
|
strings.Join(tc.expectedErrors, "\n"), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if result[i] != tc.expectedErrors[i] {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v",
|
|
strings.Join(tc.expectedErrors, "\n"), strings.Join(result, "\n"))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckUnusedImports(t *testing.T) {
|
|
cases := []strictnessTestCase{
|
|
{
|
|
note: "simple unused: input ref with same name",
|
|
module: `package p
|
|
import data.foo.bar as bar
|
|
r {
|
|
input.bar == 11
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.foo.bar as bar unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "unused import, but imported ref used",
|
|
module: `package p
|
|
import data.foo # unused
|
|
r { data.foo == 10 }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.foo unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "one of two unused",
|
|
module: `package p
|
|
import data.foo
|
|
import data.x.power #unused
|
|
r { foo == 10 }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 3, 4),
|
|
Message: "import data.x.power unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "multiple unused: with input ref of same name",
|
|
module: `package p
|
|
import data.foo
|
|
import data.x.power
|
|
r { input.foo == 10 }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.foo unused",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 3, 4),
|
|
Message: "import data.x.power unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "ignore unused rego import",
|
|
module: `package p
|
|
import rego.v1
|
|
r if { 10 == 10 }
|
|
`,
|
|
},
|
|
{
|
|
note: "import used in comparison",
|
|
module: `package p
|
|
import data.foo.x
|
|
r { x == 10 }
|
|
`,
|
|
},
|
|
{
|
|
note: "multiple used imports in one rule",
|
|
module: `package p
|
|
import data.foo.x
|
|
import data.power.ranger
|
|
r { ranger == x }
|
|
`,
|
|
},
|
|
{
|
|
note: "multiple used imports in separate rules",
|
|
module: `package p
|
|
import data.foo.x
|
|
import data.power.ranger
|
|
r { ranger == 23 }
|
|
t { x == 1 }
|
|
`,
|
|
},
|
|
{
|
|
note: "import used as function operand",
|
|
module: `package p
|
|
import data.foo
|
|
r = count(foo) > 1 # only one operand
|
|
`,
|
|
},
|
|
{
|
|
note: "import used as function operand, compount term",
|
|
module: `package p
|
|
import data.foo
|
|
r = sprintf("%v %d", [foo, 0])
|
|
`,
|
|
},
|
|
{
|
|
note: "import used as plain term",
|
|
module: `package p
|
|
import data.foo
|
|
r {
|
|
foo
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "import used in 'every' domain",
|
|
module: `package p
|
|
import future.keywords.every
|
|
import data.foo
|
|
r {
|
|
every x in foo { x > 1 }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "import used in 'every' body",
|
|
module: `package p
|
|
import future.keywords.every
|
|
import data.foo
|
|
r {
|
|
every x in [1,2,3] { x > foo }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "future import kept even if unused",
|
|
module: `package p
|
|
import future.keywords
|
|
|
|
r { true }
|
|
`,
|
|
},
|
|
{
|
|
note: "shadowed var name in function arg",
|
|
module: `package p
|
|
import data.foo # unused
|
|
|
|
r { f(1) }
|
|
f(foo) = foo == 1
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.foo unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "shadowed assigned var name",
|
|
module: `package p
|
|
import data.foo # unused
|
|
|
|
r { foo := true; foo }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.foo unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "used as rule value",
|
|
module: `package p
|
|
import data.bar # unused
|
|
import data.foo
|
|
|
|
r = foo { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.bar unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "unused as rule value (but same data ref)",
|
|
module: `package p
|
|
import data.bar # unused
|
|
import data.foo # unused
|
|
|
|
r = data.foo { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 2, 4),
|
|
Message: "import data.bar unused",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 3, 4),
|
|
Message: "import data.foo unused",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
runStrictnessTestCase(t, cases, true)
|
|
}
|
|
|
|
func TestCompilerCheckDuplicateImports(t *testing.T) {
|
|
cases := []strictnessTestCase{
|
|
{
|
|
note: "shadow",
|
|
module: `package test
|
|
import input.noconflict
|
|
import input.foo
|
|
import data.foo
|
|
import data.bar.foo
|
|
|
|
p := noconflict
|
|
q := foo
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 4, 5),
|
|
Message: "import must not shadow import input.foo",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 5, 5),
|
|
Message: "import must not shadow import input.foo",
|
|
},
|
|
},
|
|
}, {
|
|
note: "alias shadow",
|
|
module: `package test
|
|
import input.noconflict
|
|
import input.foo
|
|
import input.bar as foo
|
|
|
|
p := noconflict
|
|
q := foo
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("import"), "", 4, 5),
|
|
Message: "import must not shadow import input.foo",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
runStrictnessTestCase(t, cases, true)
|
|
}
|
|
|
|
func TestCompilerCheckKeywordOverrides(t *testing.T) {
|
|
cases := []strictnessTestCase{
|
|
{
|
|
note: "rule names",
|
|
module: `package test
|
|
input { true }
|
|
p { true }
|
|
data { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input { true }"), "", 2, 5),
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data { true }"), "", 4, 5),
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule names (set construction)",
|
|
module: `package test
|
|
input.a { true }
|
|
p { true }
|
|
data.b { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input.a { true }"), "", 2, 5),
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data.b { true }"), "", 4, 5),
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule names (object construction)",
|
|
module: `package test
|
|
input.a := 1 { true }
|
|
p { true }
|
|
data.b := 2 { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input.a := 1 { true }"), "", 2, 5),
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data.b := 2 { true }"), "", 4, 5),
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "leading term in rule refs",
|
|
module: `package test
|
|
input.a.b { true }
|
|
p { true }
|
|
data.b.c := "foo" { true }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input.a.b { true }"), "", 2, 5),
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte(`data.b.c := "foo" { true }`), "", 4, 5),
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "global assignments",
|
|
module: `package test
|
|
input = 1
|
|
p := 2
|
|
data := 3
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input = 1"), "", 2, 5),
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 4, 5),
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule-local assignments",
|
|
module: `package test
|
|
p {
|
|
input := 1
|
|
x := 2
|
|
} else {
|
|
data := 3
|
|
}
|
|
q {
|
|
input := 4
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 3, 6),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 6, 6),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 4"), "", 9, 6),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension-local assignments",
|
|
module: `package test
|
|
p = [ x |
|
|
input := 1
|
|
x := 2
|
|
data := 3
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 3, 6),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 5, 6),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension-local assignments",
|
|
module: `package test
|
|
p = { x |
|
|
input := 1
|
|
x := 2
|
|
data := 3
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 3, 6),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 5, 6),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension-local assignments",
|
|
module: `package test
|
|
p = { x: 1 |
|
|
input := 1
|
|
x := 2
|
|
data := 3
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 3, 6),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 5, 6),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "not-local assignments",
|
|
module: `package test
|
|
import future.keywords.not
|
|
p {
|
|
not { input := 1; data := 2 }
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 4, 12),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 2"), "", 4, 24),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and-local assignments",
|
|
module: `package test
|
|
import future.keywords.and
|
|
p {
|
|
{ input := 1 } and { data := 2 }
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 4, 8),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 2"), "", 4, 27),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or-local assignments",
|
|
module: `package test
|
|
import future.keywords.or
|
|
p {
|
|
{ input := 1 } or { data := 2 }
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 4, 8),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 2"), "", 4, 26),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "nested override",
|
|
module: `package test
|
|
p {
|
|
[ x |
|
|
input := 1
|
|
x := 2
|
|
data := 3
|
|
]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("input := 1"), "", 4, 7),
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
},
|
|
&Error{
|
|
Location: NewLocation([]byte("data := 3"), "", 6, 7),
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
runStrictnessTestCase(t, cases, true)
|
|
}
|
|
|
|
func TestCompilerCheckDeprecatedMethods(t *testing.T) {
|
|
cases := []strictnessTestCase{
|
|
{
|
|
note: "all() built-in",
|
|
module: `package test
|
|
p := all([true, false])
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("all([true, false])"), "", 2, 10),
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "user-defined all()",
|
|
module: `package test
|
|
import future.keywords.in
|
|
all(arr) = {x | some x in arr} == {true}
|
|
p := all([true, false])
|
|
`,
|
|
},
|
|
{
|
|
note: "any() built-in",
|
|
module: `package test
|
|
p := any([true, false])
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte("any([true, false])"), "", 2, 10),
|
|
Message: "deprecated built-in function calls in expression: any",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "user-defined any()",
|
|
module: `package test
|
|
import future.keywords.in
|
|
any(arr) := true in arr
|
|
p := any([true, false])
|
|
`,
|
|
},
|
|
{
|
|
note: "re_match built-in",
|
|
module: `package test
|
|
p := re_match("[a]", "a")
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Location: NewLocation([]byte(`re_match("[a]", "a")`), "", 2, 10),
|
|
Message: "deprecated built-in function calls in expression: re_match",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
runStrictnessTestCase(t, cases, true)
|
|
}
|
|
|
|
type strictnessTestCase struct {
|
|
note string
|
|
module string
|
|
expectedErrors Errors
|
|
}
|
|
|
|
func runStrictnessTestCase(t *testing.T, cases []strictnessTestCase, assertLocation bool) {
|
|
t.Helper()
|
|
makeTestRunner := func(tc strictnessTestCase, strict bool) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
compiler := NewCompiler().WithStrict(strict)
|
|
compiler.Modules = map[string]*Module{
|
|
"test": MustParseModuleWithOpts(tc.module, ParserOptions{
|
|
RegoVersion: RegoV0,
|
|
Capabilities: CapabilitiesForThisVersion(
|
|
CapabilitiesRegoVersion(RegoV0),
|
|
CapabilitiesExperimentalKeywords(true),
|
|
),
|
|
}),
|
|
}
|
|
compileStages(compiler, "")
|
|
|
|
if strict {
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, assertLocation)
|
|
} else {
|
|
assertNotFailed(t, compiler)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note+"_strict", makeTestRunner(tc, true))
|
|
t.Run(tc.note+"_non-strict", makeTestRunner(tc, false))
|
|
}
|
|
}
|
|
|
|
func assertErrors(t *testing.T, actual Errors, expected Errors, assertLocation bool) {
|
|
t.Helper()
|
|
if len(expected) != len(actual) {
|
|
t.Fatalf("Expected %d errors, got %d:\n\n%s\n", len(expected), len(actual), actual.Error())
|
|
}
|
|
incorrectErrs := false
|
|
for _, e := range expected {
|
|
found := false
|
|
for _, actual := range actual {
|
|
if e.Message == actual.Message {
|
|
if !assertLocation || e.Location.Equal(actual.Location) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
incorrectErrs = true
|
|
}
|
|
}
|
|
if incorrectErrs {
|
|
t.Fatalf("Expected errors:\n\n%s\n\nGot:\n\n%s\n", expected.Error(), actual.Error())
|
|
}
|
|
}
|
|
|
|
func TestCompileRegoV1Import(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
modules map[string]string
|
|
expectedErrors Errors
|
|
}{
|
|
// Duplicate imports
|
|
{
|
|
note: "duplicate imports",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.bar.foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "duplicate imports (alias)",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.bar as foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "duplicate imports (alias, different order)",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
import data.bar as foo
|
|
import data.foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.bar as foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "duplicate imports (repeat)",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "duplicate imports (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.bar.foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.bar.foo
|
|
q if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy1.rego", Row: 4, Col: 6},
|
|
},
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy2.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "duplicate imports (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import future.keywords.if
|
|
import data.foo
|
|
import data.bar.foo
|
|
p if {
|
|
foo == "bar"
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
import data.foo
|
|
import data.bar.foo
|
|
q if {
|
|
foo == "bar"
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "import must not shadow import data.foo",
|
|
Location: &Location{Text: []byte("import"), File: "policy2.rego", Row: 4, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
// var shadowing
|
|
{
|
|
note: "var shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p if {
|
|
input := 1
|
|
input == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "var shadows input (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
p if {
|
|
input := 1
|
|
input == 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q if {
|
|
input := 1
|
|
input == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy1.rego", Row: 4, Col: 7},
|
|
},
|
|
&Error{
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy2.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "var shadows input (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import future.keywords.if
|
|
p if {
|
|
input := 1
|
|
input == 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q if {
|
|
input := 1
|
|
input == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow input (use a different variable name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy2.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "var shadows data",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p if {
|
|
data := 1
|
|
data == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "var shadows data (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
p if {
|
|
data := 1
|
|
data == 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q if {
|
|
data := 1
|
|
data == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy1.rego", Row: 4, Col: 7},
|
|
},
|
|
&Error{
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy2.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "var shadows data (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import future.keywords.if
|
|
p if {
|
|
data := 1
|
|
data == 1
|
|
}`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q if {
|
|
data := 1
|
|
data == 1
|
|
}`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "variables must not shadow data (use a different variable name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy2.rego", Row: 4, Col: 7},
|
|
},
|
|
},
|
|
},
|
|
// rule shadowing
|
|
{
|
|
note: "rule shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
input := 1`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule (object) shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
input.a := "b" if { true }`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte(`input.a := "b" if { true }`), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule (set) shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
input contains "a" if { true }`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte(`input contains "a" if { true }`), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule ref shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
input.a.b.c := 1`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte("input.a.b.c := 1"), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule shadows input (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
input := 1`,
|
|
"policy2.rego": `package test2
|
|
import rego.v1
|
|
input := 2`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte("input := 1"), File: "policy1.rego", Row: 3, Col: 6},
|
|
},
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte("input := 2"), File: "policy2.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule shadows input (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
input := 1`,
|
|
"policy2.rego": `package test2
|
|
import rego.v1
|
|
input := 2`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow input (use a different rule name)",
|
|
Location: &Location{Text: []byte("input := 2"), File: "policy2.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule shadows data",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
data := 1`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule (object) shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
data.a := "b" if { true }`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte(`data.a := "b" if { true }`), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule (set) shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
data contains "a" if { true }`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte(`data contains "a" if { true }`), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule ref shadows input",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
data.a.b.c := 1`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte("data.a.b.c := 1"), File: "policy.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule shadows data (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
data := 1`,
|
|
"policy2.rego": `package test2
|
|
import rego.v1
|
|
data := 2`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte("data := 1"), File: "policy1.rego", Row: 3, Col: 6},
|
|
},
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte("data := 2"), File: "policy2.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "rule shadows data (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
data := 1`,
|
|
"policy2.rego": `package test2
|
|
import rego.v1
|
|
data := 2`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "rules must not shadow data (use a different rule name)",
|
|
Location: &Location{Text: []byte("data := 2"), File: "policy2.rego", Row: 3, Col: 6},
|
|
},
|
|
},
|
|
},
|
|
// deprecated built-ins
|
|
{
|
|
note: "deprecated built-in",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p := all([true, false])`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
Location: &Location{Text: []byte("all([true, false])"), File: "policy.rego", Row: 3, Col: 11},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "deprecated built-in (multiple)",
|
|
modules: map[string]string{
|
|
"policy.rego": `package test
|
|
import rego.v1
|
|
p := all([true, false])
|
|
q := any([true, false])`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
Location: &Location{Text: []byte("all([true, false])"), File: "policy.rego", Row: 3, Col: 11},
|
|
},
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: any",
|
|
Location: &Location{Text: []byte("any([true, false])"), File: "policy.rego", Row: 4, Col: 11},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "deprecated built-in (multiple modules)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
import rego.v1
|
|
p := all([true, false])`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q := all([true, false])`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
Location: &Location{Text: []byte("all([true, false])"), File: "policy1.rego", Row: 3, Col: 11},
|
|
},
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
Location: &Location{Text: []byte("all([true, false])"), File: "policy2.rego", Row: 3, Col: 11},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "deprecated built-in (multiple modules, not all strict)",
|
|
modules: map[string]string{
|
|
"policy1.rego": `package test
|
|
p := all([true, false])`,
|
|
"policy2.rego": `package test
|
|
import rego.v1
|
|
q := all([true, false])`,
|
|
},
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Message: "deprecated built-in function calls in expression: all",
|
|
Location: &Location{Text: []byte("all([true, false])"), File: "policy2.rego", Row: 3, Col: 11},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler()
|
|
compiler.Modules = map[string]*Module{}
|
|
for name, mod := range tc.modules {
|
|
if parsed, err := ParseModuleWithOpts(name, mod, ParserOptions{RegoVersion: RegoV0}); err != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
compiler.Modules[name] = parsed
|
|
}
|
|
}
|
|
compileStages(compiler, "")
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
// NOTE(sr): the tests below this function are unwieldy, let's keep adding new ones to this one
|
|
func TestCompilerResolveAllRefsNewTests(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
mod string
|
|
exp string
|
|
extra string
|
|
}{
|
|
{
|
|
note: "ref-rules referenced in body",
|
|
mod: `package test
|
|
a.b.c = 1
|
|
q if a.b.c == 1
|
|
`,
|
|
exp: `package test
|
|
a.b.c = 1 if { true }
|
|
q if data.test.a.b.c = 1
|
|
`,
|
|
},
|
|
{
|
|
// NOTE(sr): This is a conservative extension of how it worked before:
|
|
// we will not automatically extend references to other parts of the rule tree,
|
|
// only to ref rules defined on the same level.
|
|
note: "ref-rules from other module referenced in body",
|
|
mod: `package test
|
|
q if a.b.c == 1
|
|
`,
|
|
extra: `package test
|
|
a.b.c = 1
|
|
`,
|
|
exp: `package test
|
|
q if data.test.a.b.c = 1
|
|
`,
|
|
},
|
|
{
|
|
note: "single-value rule in comprehension in call", // NOTE(sr): this is TestRego/partialiter/objects_conflict
|
|
mod: `package test
|
|
p := count([x | q[x]])
|
|
q[1] = 1
|
|
`,
|
|
exp: `package test
|
|
p := __local0__ if { __local1__ = [x | data.test.q[x]]; count(__local1__, __local0__) }
|
|
q[1] = 1
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
c := NewCompiler()
|
|
mod, err := ParseModuleWithOpts("test.rego", tc.mod, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, err := ParseModuleWithOpts("test.rego", tc.exp, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mods := map[string]*Module{"test": mod}
|
|
if tc.extra != "" {
|
|
extra, err := ParseModuleWithOpts("test.rego", tc.extra, opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mods["extra"] = extra
|
|
}
|
|
c.Compile(mods)
|
|
if err := c.Errors; len(err) > 0 {
|
|
t.Errorf("compile module: %v", err)
|
|
}
|
|
if act := c.Modules["test"]; !exp.Equal(act) {
|
|
t.Errorf("compiled: expected %v, got %v", exp, act)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerResolveAllRefs(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = getCompilerTestModules()
|
|
c.Modules["head"] = MustParseModule(`package head
|
|
|
|
import rego.v1
|
|
import data.doc1 as bar
|
|
import input.x.y.foo
|
|
import input.qux as baz
|
|
|
|
p[foo[bar[i]]] := {"baz": baz} if { true }`)
|
|
|
|
c.Modules["elsekw"] = MustParseModule(`package elsekw
|
|
|
|
import rego.v1
|
|
import input.x.y.foo
|
|
import data.doc1 as bar
|
|
import input.baz
|
|
|
|
p if {
|
|
false
|
|
} else = foo if {
|
|
bar
|
|
} else = baz if {
|
|
true
|
|
}
|
|
`)
|
|
|
|
c.Modules["nestedexprs"] = MustParseModule(`package nestedexprs
|
|
import rego.v1
|
|
|
|
x = 1
|
|
|
|
p if {
|
|
f(g(x))
|
|
}`)
|
|
|
|
c.Modules["assign"] = MustParseModule(`package assign
|
|
import rego.v1
|
|
|
|
x = 1
|
|
y = 1
|
|
|
|
p if {
|
|
x := y
|
|
[true | x := y]
|
|
}`)
|
|
|
|
c.Modules["someinassign"] = MustParseModule(`package someinassign
|
|
import rego.v1
|
|
|
|
x = 1
|
|
y = 1
|
|
|
|
p[x] if {
|
|
some x in [1, 2, y]
|
|
}`)
|
|
|
|
c.Modules["someinassignwithkey"] = MustParseModule(`package someinassignwithkey
|
|
import rego.v1
|
|
|
|
x = 1
|
|
y = 1
|
|
|
|
p[x] if {
|
|
some k, v in [1, 2, y]
|
|
}`)
|
|
|
|
c.Modules["donotresolve"] = MustParseModule(`package donotresolve
|
|
import rego.v1
|
|
|
|
x = 1
|
|
|
|
f(x) if {
|
|
x = 2
|
|
}
|
|
`)
|
|
|
|
c.Modules["indirectrefs"] = MustParseModule(`package indirectrefs
|
|
import rego.v1
|
|
|
|
f(x) = [x] if {true}
|
|
|
|
p if {
|
|
f(1)[0]
|
|
}
|
|
`)
|
|
|
|
c.Modules["comprehensions"] = MustParseModule(`package comprehensions
|
|
import rego.v1
|
|
|
|
nums = [1, 2, 3]
|
|
|
|
f(x) = [x] if {true}
|
|
|
|
p[[1]] if {true}
|
|
|
|
q if {
|
|
p[[x | x = nums[_]]]
|
|
}
|
|
|
|
r = [y | y = f(1)[0]]
|
|
`)
|
|
|
|
c.Modules["everykw"] = MustParseModule(`package everykw
|
|
import rego.v1
|
|
|
|
nums = {1, 2, 3}
|
|
f(_) = true
|
|
x = 100
|
|
xs = [1, 2, 3]
|
|
p if {
|
|
every x in xs {
|
|
nums[x]
|
|
x > 10
|
|
}
|
|
}`)
|
|
|
|
c.Modules["heads_with_dots"] = MustParseModule(`package heads_with_dots
|
|
import rego.v1
|
|
|
|
this_is_not = true
|
|
this.is.dotted if { this_is_not }
|
|
`)
|
|
|
|
compileStages(c, StageResolveRefs)
|
|
assertNotFailed(t, c)
|
|
|
|
// Basic test cases.
|
|
mod1 := c.Modules["mod1"]
|
|
p := mod1.Rules[0]
|
|
expr1 := p.Body[0]
|
|
term := expr1.Terms.(*Term)
|
|
e := MustParseTerm("data.a.b.c.q[x]")
|
|
if !term.Equal(e) {
|
|
t.Errorf("Wrong term (global in same module): expected %v but got: %v", e, term)
|
|
}
|
|
|
|
expr2 := p.Body[1]
|
|
term = expr2.Terms.(*Term)
|
|
e = MustParseTerm("data.a.b.c.r[x]")
|
|
if !term.Equal(e) {
|
|
t.Errorf("Wrong term (global in same package/diff module): expected %v but got: %v", e, term)
|
|
}
|
|
|
|
mod2 := c.Modules["mod2"]
|
|
r := mod2.Rules[0]
|
|
expr3 := r.Body[1]
|
|
term = expr3.Terms.([]*Term)[1]
|
|
e = MustParseTerm("data.x.y.p")
|
|
if !term.Equal(e) {
|
|
t.Errorf("Wrong term (var import): expected %v but got: %v", e, term)
|
|
}
|
|
|
|
mod3 := c.Modules["mod3"]
|
|
expr4 := mod3.Rules[0].Body[0]
|
|
term = expr4.Terms.([]*Term)[2]
|
|
e = MustParseTerm("{input.x.secret: [{input.x.keyid}]}")
|
|
if !term.Equal(e) {
|
|
t.Errorf("Wrong term (nested refs): expected %v but got: %v", e, term)
|
|
}
|
|
|
|
// Array comprehensions.
|
|
mod5 := c.Modules["mod5"]
|
|
|
|
ac := func(r *Rule) *ArrayComprehension {
|
|
return r.Body[0].Terms.(*Term).Value.(*ArrayComprehension)
|
|
}
|
|
|
|
acTerm1 := ac(mod5.Rules[0])
|
|
assertTermEqual(t, acTerm1.Term, MustParseTerm("input.x.a"))
|
|
acTerm2 := ac(mod5.Rules[1])
|
|
assertTermEqual(t, acTerm2.Term, MustParseTerm("data.a.b.c.q.a"))
|
|
acTerm3 := ac(mod5.Rules[2])
|
|
assertTermEqual(t, acTerm3.Body[0].Terms.([]*Term)[1], MustParseTerm("input.x.a"))
|
|
acTerm4 := ac(mod5.Rules[3])
|
|
assertTermEqual(t, acTerm4.Body[0].Terms.([]*Term)[1], MustParseTerm("data.a.b.c.q[i]"))
|
|
acTerm5 := ac(mod5.Rules[4])
|
|
assertTermEqual(t, acTerm5.Body[0].Terms.([]*Term)[2].Value.(*ArrayComprehension).Term, MustParseTerm("input.x.a"))
|
|
acTerm6 := ac(mod5.Rules[5])
|
|
assertTermEqual(t, acTerm6.Body[0].Terms.([]*Term)[2].Value.(*ArrayComprehension).Body[0].Terms.([]*Term)[1], MustParseTerm("data.a.b.c.q[i]"))
|
|
|
|
// Nested references.
|
|
mod6 := c.Modules["mod6"]
|
|
nested1 := mod6.Rules[0].Body[0].Terms.(*Term)
|
|
assertTermEqual(t, nested1, MustParseTerm("data.x[input.x[i].a[data.z.b[j]]]"))
|
|
|
|
nested2 := mod6.Rules[1].Body[1].Terms.(*Term)
|
|
assertTermEqual(t, nested2, MustParseTerm("v[input.x[i]]"))
|
|
|
|
nested3 := mod6.Rules[3].Body[0].Terms.(*Term)
|
|
assertTermEqual(t, nested3, MustParseTerm("data.x[data.a.b.nested.r]"))
|
|
|
|
// Refs in head.
|
|
mod7 := c.Modules["head"]
|
|
assertTermEqual(t, mod7.Rules[0].Head.Key, MustParseTerm("input.x.y.foo[data.doc1[i]]"))
|
|
assertTermEqual(t, mod7.Rules[0].Head.Value, MustParseTerm(`{"baz": input.qux}`))
|
|
|
|
// Refs in else.
|
|
mod8 := c.Modules["elsekw"]
|
|
assertTermEqual(t, mod8.Rules[0].Else.Head.Value, MustParseTerm("input.x.y.foo"))
|
|
assertTermEqual(t, mod8.Rules[0].Else.Body[0].Terms.(*Term), MustParseTerm("data.doc1"))
|
|
assertTermEqual(t, mod8.Rules[0].Else.Else.Head.Value, MustParseTerm("input.baz"))
|
|
|
|
// Refs in calls.
|
|
mod9 := c.Modules["nestedexprs"]
|
|
assertTermEqual(t, mod9.Rules[1].Body[0].Terms.([]*Term)[1], CallTerm(RefTerm(VarTerm("g")), MustParseTerm("data.nestedexprs.x")))
|
|
|
|
// Ignore assigned vars.
|
|
mod10 := c.Modules["assign"]
|
|
assertTermEqual(t, mod10.Rules[2].Body[0].Terms.([]*Term)[1], VarTerm("x"))
|
|
assertTermEqual(t, mod10.Rules[2].Body[0].Terms.([]*Term)[2], MustParseTerm("data.assign.y"))
|
|
assignCompr := mod10.Rules[2].Body[1].Terms.(*Term).Value.(*ArrayComprehension)
|
|
assertTermEqual(t, assignCompr.Body[0].Terms.([]*Term)[1], VarTerm("x"))
|
|
assertTermEqual(t, assignCompr.Body[0].Terms.([]*Term)[2], MustParseTerm("data.assign.y"))
|
|
|
|
// Args
|
|
mod11 := c.Modules["donotresolve"]
|
|
assertTermEqual(t, mod11.Rules[1].Head.Args[0], VarTerm("x"))
|
|
assertExprEqual(t, mod11.Rules[1].Body[0], MustParseExpr("x = 2"))
|
|
|
|
// Locations.
|
|
parsedLoc := getCompilerTestModules()["mod1"].Rules[0].Body[0].Terms.(*Term).Value.(Ref)[0].Location
|
|
compiledLoc := c.Modules["mod1"].Rules[0].Body[0].Terms.(*Term).Value.(Ref)[0].Location
|
|
if parsedLoc.Row != compiledLoc.Row {
|
|
t.Fatalf("Expected parsed location (%v) and compiled location (%v) to be equal", parsedLoc.Row, compiledLoc.Row)
|
|
}
|
|
|
|
// Indirect references.
|
|
mod12 := c.Modules["indirectrefs"]
|
|
assertExprEqual(t, mod12.Rules[1].Body[0], MustParseExpr("data.indirectrefs.f(1)[0]"))
|
|
|
|
// Comprehensions
|
|
mod13 := c.Modules["comprehensions"]
|
|
assertExprEqual(t, mod13.Rules[3].Body[0].Terms.(*Term).Value.(Ref)[3].Value.(*ArrayComprehension).Body[0], MustParseExpr("x = data.comprehensions.nums[_]"))
|
|
assertExprEqual(t, mod13.Rules[4].Head.Value.Value.(*ArrayComprehension).Body[0], MustParseExpr("y = data.comprehensions.f(1)[0]"))
|
|
|
|
// Ignore vars assigned via `some x in xs`.
|
|
mod14 := c.Modules["someinassign"]
|
|
someInAssignCall := mod14.Rules[2].Body[0].Terms.(*SomeDecl).Symbols[0].Value.(Call)
|
|
assertTermEqual(t, someInAssignCall[1], VarTerm("x"))
|
|
collectionLastElem := someInAssignCall[2].Value.(*Array).Get(IntNumberTerm(2))
|
|
assertTermEqual(t, collectionLastElem, MustParseTerm("data.someinassign.y"))
|
|
|
|
// Ignore key and val vars assigned via `some k, v in xs`.
|
|
mod15 := c.Modules["someinassignwithkey"]
|
|
someInAssignCall = mod15.Rules[2].Body[0].Terms.(*SomeDecl).Symbols[0].Value.(Call)
|
|
assertTermEqual(t, someInAssignCall[1], VarTerm("k"))
|
|
assertTermEqual(t, someInAssignCall[2], VarTerm("v"))
|
|
collectionLastElem = someInAssignCall[3].Value.(*Array).Get(IntNumberTerm(2))
|
|
assertTermEqual(t, collectionLastElem, MustParseTerm("data.someinassignwithkey.y"))
|
|
|
|
mod16 := c.Modules["everykw"]
|
|
everyExpr := mod16.Rules[len(mod16.Rules)-1].Body[0].Terms.(*Every)
|
|
assertTermEqual(t, everyExpr.Body[0].Terms.(*Term), MustParseTerm("data.everykw.nums[x]"))
|
|
assertTermEqual(t, everyExpr.Domain, MustParseTerm("data.everykw.xs"))
|
|
|
|
// 'x' is not resolved
|
|
assertTermEqual(t, everyExpr.Value, VarTerm("x"))
|
|
gt10 := MustParseExpr("x > 10")
|
|
gt10.Index++ // TODO(sr): why?
|
|
assertExprEqual(t, everyExpr.Body[1], gt10)
|
|
|
|
// head refs are kept as-is, but their bodies are replaced.
|
|
mod := c.Modules["heads_with_dots"]
|
|
rule := mod.Rules[1]
|
|
body := rule.Body[0].Terms.(*Term)
|
|
assertTermEqual(t, body, MustParseTerm("data.heads_with_dots.this_is_not"))
|
|
if act, exp := rule.Head.Ref(), MustParseRef("this.is.dotted"); act.Compare(exp) != 0 {
|
|
t.Errorf("expected %v to match %v", act, exp)
|
|
}
|
|
}
|
|
|
|
func TestCompilerResolveErrors(t *testing.T) {
|
|
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"shadow-globals": MustParseModule(`
|
|
package shadow_globals
|
|
import rego.v1
|
|
|
|
f([input]) if { true }
|
|
`),
|
|
}
|
|
|
|
compileStages(c, StageResolveRefs)
|
|
|
|
expected := []string{
|
|
`args must not shadow input`,
|
|
}
|
|
|
|
assertCompilerErrorStrings(t, c, expected)
|
|
}
|
|
|
|
func TestCompilerRewriteTermsInHead(t *testing.T) {
|
|
popts := ParserOptions{AllFutureKeywords: true}
|
|
|
|
tests := []struct {
|
|
note string
|
|
mod *Module
|
|
exp *Rule
|
|
}{
|
|
{
|
|
note: "imports",
|
|
mod: module(`package head
|
|
import data.doc1 as bar
|
|
import data.doc2 as corge
|
|
import input.x.y.foo
|
|
import input.qux as baz
|
|
|
|
p[foo[bar[i]]] = {"baz": baz, "corge": corge} if { true }
|
|
`),
|
|
exp: MustParseRule(`p[__local0__] = __local1__ { __local0__ = input.x.y.foo[data.doc1[i]]; __local1__ = {"baz": input.qux, "corge": data.doc2} }`),
|
|
},
|
|
{
|
|
note: "array comprehension value",
|
|
mod: module(`package head
|
|
q = [true | true] if { true }
|
|
`),
|
|
exp: MustParseRule(`q = __local0__ { __local0__ = [true | true] }`),
|
|
},
|
|
{
|
|
note: "array comprehension value in else head",
|
|
mod: module(`package head
|
|
q if {
|
|
false
|
|
} else = [true | true] if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = true { false } else = __local0__ { __local0__ = [true | true] }`),
|
|
},
|
|
{
|
|
note: "array comprehension value in head (comprehension-local var)",
|
|
mod: module(`package head
|
|
q = [a | a := true] if {
|
|
false
|
|
} else = [a | a := true] if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = __local2__ { false; __local2__ = [__local0__ | __local0__ = true] } else = __local3__ { __local3__ = [__local1__ | __local1__ = true] }`),
|
|
},
|
|
{
|
|
note: "array comprehension value in function head (comprehension-local var)",
|
|
mod: module(`package head
|
|
f(x) = [a | a := true] if {
|
|
false
|
|
} else = [a | a := true] if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__) = __local3__ { false; __local3__ = [__local1__ | __local1__ = true] } else = __local4__ { __local4__ = [__local2__ | __local2__ = true] }`),
|
|
},
|
|
{
|
|
note: "array comprehension value in else-func head (reused arg rewrite)",
|
|
mod: module(`package head
|
|
f(x, y) = [x | y] if {
|
|
false
|
|
} else = [x | y] if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__, __local1__) = __local2__ { false; __local2__ = [__local0__ | __local1__] } else = __local3__ { __local3__ = [__local0__ | __local1__] }`),
|
|
},
|
|
{
|
|
note: "object comprehension value",
|
|
mod: module(`package head
|
|
r = {"true": true | true} if { true }
|
|
`),
|
|
exp: MustParseRule(`r = __local0__ { __local0__ = {"true": true | true} }`),
|
|
},
|
|
{
|
|
note: "object comprehension value in else head",
|
|
mod: module(`package head
|
|
q if {
|
|
false
|
|
} else = {"true": true | true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = true { false } else = __local0__ { __local0__ = {"true": true | true} }`),
|
|
},
|
|
{
|
|
note: "object comprehension value in head (comprehension-local var)",
|
|
mod: module(`package head
|
|
q = {"a": a | a := true} if {
|
|
false
|
|
} else = {"a": a | a := true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = __local2__ { false; __local2__ = {"a": __local0__ | __local0__ = true} } else = __local3__ { __local3__ = {"a": __local1__ | __local1__ = true} }`),
|
|
},
|
|
{
|
|
note: "object comprehension value in function head (comprehension-local var)",
|
|
mod: module(`package head
|
|
f(x) = {"a": a | a := true} if {
|
|
false
|
|
} else = {"a": a | a := true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__) = __local3__ { false; __local3__ = {"a": __local1__ | __local1__ = true} } else = __local4__ { __local4__ = {"a": __local2__ | __local2__ = true} }`),
|
|
},
|
|
{
|
|
note: "object comprehension value in else-func head (reused arg rewrite)",
|
|
mod: module(`package head
|
|
f(x, y) = {x: y | true} if {
|
|
false
|
|
} else = {x: y | true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__, __local1__) = __local2__ { false; __local2__ = {__local0__: __local1__ | true} } else = __local3__ { __local3__ = {__local0__: __local1__ | true} }`),
|
|
},
|
|
{
|
|
note: "set comprehension value",
|
|
mod: module(`package head
|
|
s = {true | true} if { true }
|
|
`),
|
|
exp: MustParseRule(`s = __local0__ { __local0__ = {true | true} }`),
|
|
},
|
|
{
|
|
note: "set comprehension value in else head",
|
|
mod: module(`package head
|
|
q = {false | false} if {
|
|
false
|
|
} else = {true | true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = __local0__ { false; __local0__ = {false | false} } else = __local1__ { __local1__ = {true | true} }`),
|
|
},
|
|
{
|
|
note: "set comprehension value in head (comprehension-local var)",
|
|
mod: module(`package head
|
|
q = {a | a := true} if {
|
|
false
|
|
} else = {a | a := true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`q = __local2__ { false; __local2__ = {__local0__ | __local0__ = true} } else = __local3__ { __local3__ = {__local1__ | __local1__ = true} }`),
|
|
},
|
|
{
|
|
note: "set comprehension value in function head (comprehension-local var)",
|
|
mod: module(`package head
|
|
f(x) = {a | a := true} if {
|
|
false
|
|
} else = {a | a := true} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__) = __local3__ { false; __local3__ = {__local1__ | __local1__ = true} } else = __local4__ { __local4__ = {__local2__ | __local2__ = true} }`),
|
|
},
|
|
{
|
|
note: "set comprehension value in else-func head (reused arg rewrite)",
|
|
mod: module(`package head
|
|
f(x, y) = {x | y} if {
|
|
false
|
|
} else = {x | y} if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`f(__local0__, __local1__) = __local2__ { false; __local2__ = {__local0__ | __local1__} } else = __local3__ { __local3__ = {__local0__ | __local1__} }`),
|
|
},
|
|
{
|
|
note: "import in else value",
|
|
mod: module(`package head
|
|
import input.qux as baz
|
|
elsekw if {
|
|
false
|
|
} else = baz if {
|
|
true
|
|
}
|
|
`),
|
|
exp: MustParseRule(`elsekw { false } else = __local0__ { __local0__ = input.qux }`),
|
|
},
|
|
{
|
|
note: "import ref in last ref head term",
|
|
mod: module(`package head
|
|
import data.doc1 as bar
|
|
x.y.z[bar[i]] = true
|
|
`),
|
|
exp: MustParseRule(`x.y.z[__local0__] = true { __local0__ = data.doc1[i] }`),
|
|
},
|
|
{
|
|
note: "import ref in multi-value ref rule",
|
|
mod: module(`package head
|
|
import data.doc1 as bar
|
|
x.y.w contains bar[i] if true
|
|
`),
|
|
exp: func() *Rule {
|
|
exp, _ := ParseRuleWithOpts(`x.y.w contains __local0__ if { __local0__ = data.doc1[i] }`, popts)
|
|
return exp
|
|
}(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules["head"] = tc.mod
|
|
compileStages(c, StageRewriteRefsInHead)
|
|
assertNotFailed(t, c)
|
|
act := c.Modules["head"].Rules[0]
|
|
assertRulesEqual(t, act, tc.exp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRefHeadsNeedCapability(t *testing.T) {
|
|
popts := ParserOptions{AllFutureKeywords: true}
|
|
for _, tc := range []struct {
|
|
note string
|
|
mod *Module
|
|
err string
|
|
}{
|
|
{
|
|
note: "one-dot ref, single-value rule, short+compat",
|
|
mod: MustParseModule(`package t
|
|
p[1] = 2`),
|
|
},
|
|
{
|
|
note: "function, short",
|
|
mod: MustParseModule(`package t
|
|
p(1)`),
|
|
},
|
|
{
|
|
note: "function",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
p(1) if true`, popts),
|
|
},
|
|
{
|
|
note: "function with value",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
p(1) = 2 if true`, popts),
|
|
},
|
|
{
|
|
note: "function with value",
|
|
mod: MustParseModule(`package t
|
|
p(1) = 2`),
|
|
},
|
|
{
|
|
note: "one-dot ref, single-value rule, compat",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
p[3] = 4 if true`, popts),
|
|
},
|
|
{
|
|
note: "multi-value non-ref head",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
p contains 1 if true`, popts),
|
|
},
|
|
{ // NOTE(sr): this was previously forbidden because we need the `if` for disambiguation
|
|
note: "one-dot ref head",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
p[1] if true`, popts),
|
|
err: "rule heads with refs are not supported: p[1]",
|
|
},
|
|
{
|
|
note: "single-value ref rule",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
a.b.c[x] if x := input`, popts),
|
|
err: "rule heads with refs are not supported: a.b.c[x]",
|
|
},
|
|
{
|
|
note: "ref head function",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
a.b.c(x) if x == input`, popts),
|
|
err: "rule heads with refs are not supported: a.b.c",
|
|
},
|
|
{
|
|
note: "multi-value ref rule",
|
|
mod: MustParseModuleWithOpts(`package t
|
|
a.b.c contains x if x := input`, popts),
|
|
err: "rule heads with refs are not supported: a.b.c",
|
|
},
|
|
} {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
caps, err := LoadCapabilitiesVersion("v0.44.0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := NewCompiler().WithCapabilities(caps)
|
|
c.Modules["test"] = tc.mod
|
|
compileStages(c, StageRewriteRefsInHead)
|
|
if tc.err != "" {
|
|
assertErrorWithMessage(t, c.Errors, tc.err)
|
|
} else {
|
|
assertNotFailed(t, c)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteRegoMetadataCalls(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "rego.metadata called, no metadata",
|
|
module: `package test
|
|
|
|
p if {
|
|
rego.metadata.chain()[0].path == ["test", "p"]
|
|
rego.metadata.rule() == {}
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local2__ = [{"path": ["test", "p"]}]
|
|
__local3__ = {}
|
|
__local0__ = __local2__
|
|
equal(__local0__[0].path, ["test", "p"])
|
|
__local1__ = __local3__
|
|
equal(__local1__, {})
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata called, no output var, no metadata",
|
|
module: `package test
|
|
|
|
p if {
|
|
rego.metadata.chain()
|
|
rego.metadata.rule()
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local0__ = [{"path": ["test", "p"]}]
|
|
__local1__ = {}
|
|
__local0__
|
|
__local1__
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata called, with metadata",
|
|
module: `# METADATA
|
|
# description: A test package
|
|
package test
|
|
|
|
# METADATA
|
|
# title: My P Rule
|
|
p if {
|
|
rego.metadata.chain()[0].title == "My P Rule"
|
|
rego.metadata.chain()[1].description == "A test package"
|
|
}
|
|
|
|
# METADATA
|
|
# title: My Other P Rule
|
|
p if {
|
|
rego.metadata.rule().title == "My Other P Rule"
|
|
}`,
|
|
exp: `# METADATA
|
|
# {"scope":"package","description":"A test package"}
|
|
package test
|
|
|
|
# METADATA
|
|
# {"scope":"rule","title":"My P Rule"}
|
|
p = true if {
|
|
__local3__ = [
|
|
{"annotations": {"scope": "rule", "title": "My P Rule"}, "path": ["test", "p"]},
|
|
{"annotations": {"description": "A test package", "scope": "package"}, "path": ["test"]}
|
|
]
|
|
__local0__ = __local3__
|
|
equal(__local0__[0].title, "My P Rule")
|
|
__local1__ = __local3__
|
|
equal(__local1__[1].description, "A test package")
|
|
}
|
|
|
|
# METADATA
|
|
# {"scope":"rule","title":"My Other P Rule"}
|
|
p = true if {
|
|
__local4__ = {"scope": "rule", "title": "My Other P Rule"}
|
|
__local2__ = __local4__
|
|
equal(__local2__.title, "My Other P Rule")
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata referenced multiple times",
|
|
module: `# METADATA
|
|
# description: TEST
|
|
package test
|
|
|
|
p if {
|
|
rego.metadata.chain()[0].path == ["test", "p"]
|
|
rego.metadata.chain()[1].path == ["test"]
|
|
}`,
|
|
exp: `# METADATA
|
|
# {"scope":"package","description":"TEST"}
|
|
package test
|
|
|
|
p = true if {
|
|
__local2__ = [
|
|
{"path": ["test", "p"]},
|
|
{"annotations": {"description": "TEST", "scope": "package"}, "path": ["test"]}
|
|
]
|
|
__local0__ = __local2__
|
|
equal(__local0__[0].path, ["test", "p"])
|
|
__local1__ = __local2__
|
|
equal(__local1__[1].path, ["test"]) }`,
|
|
},
|
|
{
|
|
note: "rego.metadata return value",
|
|
module: `package test
|
|
|
|
p := rego.metadata.chain()`,
|
|
exp: `package test
|
|
|
|
p := __local0__ if {
|
|
__local1__ = [{"path": ["test", "p"]}]
|
|
__local0__ = __local1__
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata argument in function call",
|
|
module: `package test
|
|
|
|
p if {
|
|
q(rego.metadata.chain())
|
|
}
|
|
|
|
q(s) if {
|
|
s == ["test", "p"]
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local2__ = [{"path": ["test", "p"]}]
|
|
__local1__ = __local2__
|
|
data.test.q(__local1__)
|
|
}
|
|
|
|
q(__local0__) = true if {
|
|
equal(__local0__, ["test", "p"])
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in array comprehension",
|
|
module: `package test
|
|
|
|
p = [x | x := rego.metadata.chain()]`,
|
|
exp: `package test
|
|
|
|
p = [__local0__ | __local1__ = __local2__; __local0__ = __local1__] if {
|
|
__local2__ = [{"path": ["test", "p"]}]
|
|
true
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in nested array comprehension",
|
|
module: `package test
|
|
|
|
p if {
|
|
y := [x | x := rego.metadata.chain()]
|
|
y[0].path == ["test", "p"]
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local3__ = [{"path": ["test", "p"]}];
|
|
__local1__ = [__local0__ | __local2__ = __local3__; __local0__ = __local2__];
|
|
equal(__local1__[0].path, ["test", "p"])
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in set comprehension",
|
|
module: `package test
|
|
|
|
p = {x | x := rego.metadata.chain()}`,
|
|
exp: `package test
|
|
|
|
p = {__local0__ | __local1__ = __local2__; __local0__ = __local1__} if {
|
|
__local2__ = [{"path": ["test", "p"]}]
|
|
true
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in nested set comprehension",
|
|
module: `package test
|
|
|
|
p if {
|
|
y := {x | x := rego.metadata.chain()}
|
|
y[0].path == ["test", "p"]
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local3__ = [{"path": ["test", "p"]}]
|
|
__local1__ = {__local0__ | __local2__ = __local3__; __local0__ = __local2__}
|
|
equal(__local1__[0].path, ["test", "p"])
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in object comprehension",
|
|
module: `package test
|
|
|
|
p = {i: x | x := rego.metadata.chain()[i]}`,
|
|
exp: `package test
|
|
|
|
p = {i: __local0__ | __local1__ = __local2__; __local0__ = __local1__[i]} if {
|
|
__local2__ = [{"path": ["test", "p"]}]
|
|
true
|
|
}`,
|
|
},
|
|
{
|
|
note: "rego.metadata used in nested object comprehension",
|
|
module: `package test
|
|
|
|
p if {
|
|
y := {i: x | x := rego.metadata.chain()[i]}
|
|
y[0].path == ["test", "p"]
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local3__ = [{"path": ["test", "p"]}]
|
|
__local1__ = {i: __local0__ | __local2__ = __local3__; __local0__ = __local2__[i]}
|
|
equal(__local1__[0].path, ["test", "p"])
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"test.rego": module(tc.module),
|
|
}
|
|
compileStages(c, StageRewriteRegoMetadataCalls)
|
|
assertNotFailed(t, c)
|
|
|
|
result := c.Modules["test.rego"]
|
|
exp := MustParseModuleWithOpts(tc.exp, ParserOptions{
|
|
AllFutureKeywords: true,
|
|
ProcessAnnotation: true,
|
|
})
|
|
|
|
if result.Compare(exp) != 0 {
|
|
t.Fatalf("\nExpected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerOverridingSelfCalls(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"self.rego": MustParseModule(`package self.metadata
|
|
|
|
chain(x) = "foo"
|
|
rule := "bar"`),
|
|
"test.rego": MustParseModule(`package test
|
|
import data.self
|
|
|
|
p := self.metadata.chain(42)
|
|
q := self.metadata.rule`),
|
|
}
|
|
|
|
compileStages(c, "")
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
func TestCompilerRewriteLocalAssignments(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
module string
|
|
exp any
|
|
expRewrittenMap map[Var]Var
|
|
regoVersion RegoVersion
|
|
}{
|
|
{
|
|
module: `
|
|
package test
|
|
body if { a := 1; a > 0 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
body = true if { __local0__ = 1; gt(__local0__, 0) }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
head_vars(a) = b if { b := a }
|
|
`,
|
|
exp: `
|
|
package test
|
|
head_vars(__local0__) = __local1__ if { __local1__ = __local0__ }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("b"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
head_key contains a if { a := 1 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
head_key contains __local0__ if { __local0__ = 1 }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
head_unsafe_var contains a if { some a }
|
|
`,
|
|
exp: `
|
|
package test
|
|
head_unsafe_var contains __local0__ if { true }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
p = {1,2,3}
|
|
x = 4
|
|
head_nested contains p[x] if {
|
|
some x
|
|
}`,
|
|
exp: `
|
|
package test
|
|
p = {1,2,3}
|
|
x = 4
|
|
head_nested contains data.test.p[__local0__]
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
p = {1,2}
|
|
head_closure_nested contains p[x] if {
|
|
y = [true | some x; x = 1]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = {1,2}
|
|
head_closure_nested contains data.test.p[x] if {
|
|
y = [true | __local0__ = 1]
|
|
}
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
nested if {
|
|
a := [1,2,3]
|
|
x := [true | a[i] > 1]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
nested = true if { __local0__ = [1, 2, 3]; __local1__ = [true | gt(__local0__[i], 1)] }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
x = 2
|
|
shadow_globals contains x if { x := 1 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
x = 2 if { true }
|
|
shadow_globals contains __local0__ if { __local0__ = 1 }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
shadow_rule contains shadow_rule if { shadow_rule := 1 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
shadow_rule contains __local0__ if { __local0__ = 1 }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("shadow_rule"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
shadow_roots_1 { data := 1; input := 2; input > data }
|
|
`,
|
|
exp: `
|
|
package test
|
|
shadow_roots_1 = true { __local0__ = 1; __local1__ = 2; gt(__local1__, __local0__) }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("data"),
|
|
Var("__local1__"): Var("input"),
|
|
},
|
|
regoVersion: RegoV0, // shadowing only allowed in v0
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
shadow_roots_2 { input := {"a": 1}; input.a > 0 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
shadow_roots_2 = true { __local0__ = {"a": 1}; gt(__local0__.a, 0) }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("input"),
|
|
},
|
|
regoVersion: RegoV0, // shadowing only allowed in v0
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
skip_with_target {
|
|
a := 1
|
|
input := 2
|
|
data.p with input as a
|
|
data.p with input.foo as a
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
skip_with_target = true { __local0__ = 1; __local1__ = 2; data.p with input as __local0__; data.p with input.foo as __local0__ }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("input"),
|
|
},
|
|
regoVersion: RegoV0, // shadowing only allowed in v0
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
shadow_comprehensions if {
|
|
a := 1
|
|
[true | a := 2; b := 1]
|
|
b := 2
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
shadow_comprehensions = true if { __local0__ = 1; [true | __local1__ = 2; __local2__ = 1]; __local3__ = 2 }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("a"),
|
|
Var("__local2__"): Var("b"),
|
|
Var("__local3__"): Var("b"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
scoping if {
|
|
[true | a := 1]
|
|
[true | a := 2]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
scoping = true if { [true | __local0__ = 1]; [true | __local1__ = 2] }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
object_keys if {
|
|
{k: v1, "k2": v2} := {"foo": 1, "k2": 2}
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
object_keys = true if { {"k2": __local0__, k: __local1__} = {"foo": 1, "k2": 2} }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("v2"),
|
|
Var("__local1__"): Var("v1"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
head_array_comprehensions = [[x] | x := 1]
|
|
head_set_comprehensions = {[x] | x := 1}
|
|
head_object_comprehensions = {k: [x] | k := "foo"; x := 1}
|
|
`,
|
|
exp: `
|
|
package test
|
|
head_array_comprehensions = [[__local0__] | __local0__ = 1] if { true }
|
|
head_set_comprehensions = {[__local1__] | __local1__ = 1} if { true }
|
|
head_object_comprehensions = {__local2__: [__local3__] | __local2__ = "foo"; __local3__ = 1} if { true }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
Var("__local1__"): Var("x"),
|
|
Var("__local2__"): Var("k"),
|
|
Var("__local3__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewritten_object_key if {
|
|
k := "foo"
|
|
{k: 1}
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewritten_object_key = true if { __local0__ = "foo"; {__local0__: 1} }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("k"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewritten_object_key_head contains [{k: 1}] if {
|
|
k := "foo"
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewritten_object_key_head contains [{__local0__: 1}] if { __local0__ = "foo" }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("k"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewritten_object_key_head_value = [{k: 1}] if {
|
|
k := "foo"
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewritten_object_key_head_value = [{__local0__: 1}] if { __local0__ = "foo" }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("k"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
skip_with_target_in_assignment {
|
|
input := 1
|
|
a := [true | true with input as 2; true with input.foo as 3]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
skip_with_target_in_assignment = true { __local0__ = 1; __local1__ = [true | true with input as 2; true with input.foo as 3] }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("input"),
|
|
Var("__local1__"): Var("a"),
|
|
},
|
|
regoVersion: RegoV0, // shadowing only allowed in v0
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewrite_with_value_in_assignment if {
|
|
a := 1
|
|
b := 1 with input as [a]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewrite_with_value_in_assignment = true if { __local0__ = 1; __local1__ = 1 with input as [__local0__] }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
Var("__local1__"): Var("b"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewrite_with_value_in_expr if {
|
|
a := 1
|
|
a > 0 with input as [a]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewrite_with_value_in_expr = true if { __local0__ = 1; gt(__local0__, 0) with input as [__local0__] }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
rewrite_nested_with_value_in_expr if {
|
|
a := 1
|
|
a > 0 with input as object.union({"a": a}, {"max_a": max([a])})
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
rewrite_nested_with_value_in_expr = true if { __local0__ = 1; gt(__local0__, 0) with input as object.union({"a": __local0__}, {"max_a": max([__local0__])}) }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("a"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
global = {}
|
|
ref_shadowed if {
|
|
global := {"a": 1}
|
|
global.a > 0
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
global = {} if { true }
|
|
ref_shadowed = true if { __local0__ = {"a": 1}; gt(__local0__.a, 0) }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("global"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
f(x) = y if {
|
|
x == 1
|
|
y := 2
|
|
} else = y if {
|
|
x == 3
|
|
y := 4
|
|
}
|
|
`,
|
|
// Each "else" rule has a separate rule head and the vars in the
|
|
// args will be rewritten. Since we cannot currently redefine the
|
|
// args, we must parse the module and then manually update the args.
|
|
exp: func() *Module {
|
|
module := module(`
|
|
package test
|
|
|
|
f(__local0__) = __local1__ if { __local0__ == 1; __local1__ = 2 } else = __local2__ if { __local0__ == 3; __local2__ = 4 }
|
|
`)
|
|
module.Rules[0].Else.Head.Args[0].Value = Var("__local0__")
|
|
return module
|
|
},
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
Var("__local1__"): Var("y"),
|
|
Var("__local2__"): Var("y"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
f({"x": [x]}) = y if { x == 1; y := 2 }`,
|
|
exp: `
|
|
package test
|
|
|
|
f({"x": [__local0__]}) = __local1__ if { __local0__ == 1; __local1__ = 2 }`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
Var("__local1__"): Var("y"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
|
|
f(x, [x]) = x if { x == 1 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
f(__local0__, [__local0__]) = __local0__ if { __local0__ == 1 }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
|
|
f(x) = {x[0]: 1} if { true }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
f(__local0__) = {__local0__[0]: 1} if { true }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("x"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
|
|
f({{t | t := 0}: 1}) if {
|
|
true
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
f({{__local0__ | __local0__ = 0}: 1}) if { true }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("t"),
|
|
},
|
|
},
|
|
{
|
|
module: `
|
|
package test
|
|
|
|
f({{t | t := 0}}) if {
|
|
true
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
f({{__local0__ | __local0__ = 0}}) if { true }
|
|
`,
|
|
expRewrittenMap: map[Var]Var{
|
|
Var("__local0__"): Var("t"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, tc := range tests {
|
|
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
|
setRegoVersion := func(po ParserOptions) ParserOptions {
|
|
po.RegoVersion = tc.regoVersion
|
|
return po
|
|
}
|
|
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"test.rego": module(tc.module, setRegoVersion),
|
|
}
|
|
compileStages(c, StageRewriteLocalVars)
|
|
assertNotFailed(t, c)
|
|
result := c.Modules["test.rego"]
|
|
var exp *Module
|
|
switch e := tc.exp.(type) {
|
|
case string:
|
|
exp = module(e, setRegoVersion)
|
|
case func() *Module:
|
|
exp = e()
|
|
default:
|
|
panic("expected value must be string or func() *Module")
|
|
}
|
|
if result.Compare(exp) != 0 {
|
|
t.Fatalf("\nExpected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
if !maps.Equal(c.RewrittenVars, tc.expRewrittenMap) {
|
|
t.Fatalf("\nExpected Rewritten Vars:\n\n\t%+v\n\nGot:\n\n\t%+v\n\n", tc.expRewrittenMap, c.RewrittenVars)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestRewriteLocalVarDeclarationErrors(t *testing.T) {
|
|
|
|
c := NewCompiler()
|
|
|
|
c.Modules["test"] = module(`package test
|
|
|
|
redeclaration if {
|
|
r1 = 1
|
|
r1 := 2
|
|
r2 := 1
|
|
[b, r2] := [1, 2]
|
|
foo.path == 1
|
|
foo := "foo"
|
|
_ := [1 | nested := 1; nested := 2]
|
|
}
|
|
|
|
negation if {
|
|
not a := 1
|
|
}
|
|
|
|
bad_assign if {
|
|
null := x
|
|
true := x
|
|
4.5 := x
|
|
"foo" := x
|
|
[true | true] := []
|
|
{true | true} := set()
|
|
{"foo": true | true} := {}
|
|
x + 1 := 2
|
|
data.foo := 1
|
|
[z, 1] := [1, 2]
|
|
}
|
|
|
|
arg_redeclared(arg1) if {
|
|
arg1 := 1
|
|
}
|
|
|
|
arg_nested_redeclared({{arg_nested| arg_nested := 1; arg_nested := 2}}) if { true }
|
|
`)
|
|
|
|
compileStages(c, StageRewriteLocalVars)
|
|
|
|
expectedErrors := []string{
|
|
"var r1 referenced above",
|
|
"var r2 assigned above",
|
|
"var foo referenced above",
|
|
"var nested assigned above",
|
|
"arg arg1 redeclared",
|
|
"var arg_nested assigned above",
|
|
"cannot assign vars inside negated expression",
|
|
"cannot assign to ref",
|
|
"cannot assign to arraycomprehension",
|
|
"cannot assign to setcomprehension",
|
|
"cannot assign to objectcomprehension",
|
|
"cannot assign to call",
|
|
"cannot assign to number",
|
|
"cannot assign to number",
|
|
"cannot assign to boolean",
|
|
"cannot assign to string",
|
|
"cannot assign to null",
|
|
}
|
|
|
|
sort.Strings(expectedErrors)
|
|
|
|
result := make([]string, 0, len(c.Errors))
|
|
|
|
for i := range c.Errors {
|
|
result = append(result, c.Errors[i].Message)
|
|
}
|
|
|
|
sort.Strings(result)
|
|
|
|
if len(expectedErrors) != len(result) {
|
|
t.Fatalf("Expected %d errors but got %d:\n\n%v\n\nGot:\n\n%v", len(expectedErrors), len(result), strings.Join(expectedErrors, "\n"), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if result[i] != expectedErrors[i] {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", strings.Join(expectedErrors, "\n"), strings.Join(result, "\n"))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRewriteDeclaredVarsStage(t *testing.T) {
|
|
|
|
// Unlike the following test case, this only executes up to the
|
|
// RewriteLocalVars stage. This is done so that later stages like
|
|
// RewriteDynamics are not executed.
|
|
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "object ref key",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
a := {"a": "a"}
|
|
{a.a: a.a}
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p if {
|
|
__local0__ = {"a": "a"}
|
|
{__local0__.a: __local0__.a}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "set ref element",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
a := {"a": "a"}
|
|
{a.a}
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p if {
|
|
__local0__ = {"a": "a"}
|
|
{__local0__.a}
|
|
}
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
|
|
c := NewCompiler()
|
|
|
|
c.Modules = map[string]*Module{
|
|
"test.rego": module(tc.module),
|
|
}
|
|
|
|
compileStages(c, StageRewriteLocalVars)
|
|
|
|
exp := module(tc.exp)
|
|
result := c.Modules["test.rego"]
|
|
|
|
if !exp.Equal(result) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRewriteLocalVarsBuiltinShadowing(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "unify in body shadows built-in",
|
|
module: `
|
|
package test
|
|
|
|
p := count if { count = 5 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p := __local0__ if { __local0__ = 5 }
|
|
`,
|
|
},
|
|
{
|
|
note: "shadowing var used as ref head",
|
|
module: `
|
|
package test
|
|
|
|
p := v if { count = [1, 2, 3]; v := count[0] }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p := __local1__ if { __local0__ = [1, 2, 3]; __local1__ = __local0__[0] }
|
|
`,
|
|
},
|
|
{
|
|
note: "shadow in function else-branch (issue #3729)",
|
|
module: `
|
|
package test
|
|
|
|
f(x, y, 1) := v if {
|
|
v := x[y]
|
|
} else := count if {
|
|
count = y
|
|
} else := [v] if {
|
|
v := x[y[0]]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
f(__local0__, __local1__, 1) := __local2__ if { __local2__ = __local0__[__local1__] } else = __local3__ if { __local3__ = __local1__ } else = [__local4__] if { __local4__ = __local0__[__local1__[0]] }
|
|
`,
|
|
},
|
|
{
|
|
note: "built-in call operator is preserved",
|
|
module: `
|
|
package test
|
|
|
|
p := n if { xs := [1, 2, 3]; n := count(xs) }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p := __local1__ if { __local0__ = [1, 2, 3]; __local1__ = count(__local0__) }
|
|
`,
|
|
},
|
|
{
|
|
note: "built-in name as function mock is preserved",
|
|
module: `
|
|
package test
|
|
|
|
orig(x) := concat("", x)
|
|
p := y if { y := orig(["a", "b"]) with orig as count }
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
orig(__local0__) := concat("", __local0__)
|
|
p := __local1__ if { __local1__ = data.test.orig(["a", "b"]) with data.test.orig as count }
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
|
|
c.Modules = map[string]*Module{
|
|
"test.rego": module(tc.module),
|
|
}
|
|
|
|
compileStages(c, StageRewriteLocalVars)
|
|
|
|
if len(c.Errors) > 0 {
|
|
t.Fatalf("Unexpected compile errors: %v", c.Errors)
|
|
}
|
|
|
|
exp := module(tc.exp)
|
|
result := c.Modules["test.rego"]
|
|
|
|
if exp.String() != result.String() {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompileBuiltinShadowingDeterministic(t *testing.T) {
|
|
mod := `package test
|
|
|
|
f(x, y, 1) := v if {
|
|
v := x[y]
|
|
} else := count if {
|
|
count = y
|
|
} else := [v] if {
|
|
v := x[y[0]]
|
|
}`
|
|
|
|
for i := range 50 {
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{"test.rego": module(mod)})
|
|
if c.Failed() {
|
|
t.Fatalf("run %d: unexpected compile errors: %v", i, c.Errors)
|
|
}
|
|
tpe := c.TypeEnv.Get(MustParseRef("data.test.f"))
|
|
if _, ok := tpe.(*types.Function); !ok {
|
|
t.Fatalf("run %d: expected data.test.f to be a function type, got %v", i, tpe)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompileBuiltinShadowingHeadOnly(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{"test.rego": module(`package test
|
|
|
|
p := count if { true }`)})
|
|
|
|
if !c.Failed() {
|
|
t.Fatal("expected compile to fail with unsafe var error")
|
|
}
|
|
|
|
var found bool
|
|
for _, err := range c.Errors {
|
|
if err.Code == UnsafeVarErr {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected unsafe var error, got: %v", c.Errors)
|
|
}
|
|
}
|
|
|
|
func TestRewriteDeclaredVars(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
wantErr error
|
|
}{
|
|
{
|
|
note: "rewrite unify",
|
|
module: `
|
|
package test
|
|
x = 1
|
|
y = 2
|
|
p if { some x; input = [x, y] }
|
|
`,
|
|
exp: `
|
|
package test
|
|
x = 1
|
|
y = 2
|
|
p if { __local1__ = data.test.y; input = [__local0__, __local1__] }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite call",
|
|
module: `
|
|
package test
|
|
x = []
|
|
y = {}
|
|
p if { some x; walk(y, [x, y]) }
|
|
`,
|
|
exp: `
|
|
package test
|
|
x = []
|
|
y = {}
|
|
p if { __local1__ = data.test.y; __local2__ = data.test.y; walk(__local1__, [__local0__, __local2__]) }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite term",
|
|
module: `
|
|
package test
|
|
x = "a"
|
|
y = 1
|
|
q contains [2, "b"]
|
|
p if { some x; q[[y,x]] }
|
|
`,
|
|
exp: `
|
|
package test
|
|
x = "a"
|
|
y = 1
|
|
q contains [2, "b"]
|
|
p if { __local1__ = data.test.y; data.test.q[[__local1__, __local0__]] }
|
|
`,
|
|
},
|
|
{
|
|
note: "with: rewrite target",
|
|
module: `
|
|
package test
|
|
p if {
|
|
x := "foo"
|
|
true with input[x] as 1
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local0__ = "foo";
|
|
true with input[__local0__] as 1
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "with: rewrite target in comprehension term",
|
|
module: `
|
|
package test
|
|
p if {
|
|
foo := "bar"
|
|
{ { 2 | true with input[foo] as 1} | true }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local0__ = "bar"
|
|
{__local1__ | __local1__ = { 2 | true with input[__local0__] as 1 }}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "single-value rule with ref head",
|
|
module: `
|
|
package test
|
|
|
|
p.r.q[s] = t if {
|
|
t := 1
|
|
s := input.foo
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p.r.q[__local1__] = __local0__ if {
|
|
__local0__ = 1
|
|
__local1__ = input.foo
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite some x in xs",
|
|
module: `
|
|
package test
|
|
import future.keywords.in
|
|
xs = ["a", "b", "c"]
|
|
p if { some x in xs; x == "a" }
|
|
`,
|
|
exp: `
|
|
package test
|
|
xs = ["a", "b", "c"]
|
|
p if { __local2__ = data.test.xs[__local1__]; __local2__ = "a" }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite some k, x in xs",
|
|
module: `
|
|
package test
|
|
import future.keywords.in
|
|
xs = ["a", "b", "c"]
|
|
p if { some k, x in xs; x == "a"; k == 2 }
|
|
`,
|
|
exp: `
|
|
package test
|
|
xs = ["a", "b", "c"]
|
|
p if { __local1__ = data.test.xs[__local0__]; __local1__ = "a"; __local0__ = 2 }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite some k, x in xs[i]",
|
|
module: `
|
|
package test
|
|
import future.keywords.in
|
|
xs = [["a", "b", "c"], []]
|
|
p if {
|
|
some i
|
|
some k, x in xs[i]
|
|
x == "a"
|
|
k == 2
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
xs = [["a", "b", "c"], []]
|
|
p = true if { __local2__ = data.test.xs[__local0__][__local1__]; __local2__ = "a"; __local1__ = 2 }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite some k, x in xs[i] with `i` as ref",
|
|
module: `
|
|
package test
|
|
import future.keywords.in
|
|
i = 0
|
|
xs = [["a", "b", "c"], []]
|
|
p if {
|
|
some k, x in xs[i]
|
|
x == "a"
|
|
k == 2
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
i = 0
|
|
xs = [["a", "b", "c"], []]
|
|
p = true if { __local2__ = data.test.i; __local1__ = data.test.xs[__local2__][__local0__]; __local1__ = "a"; __local0__ = 2 }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite some: with modifier on domain",
|
|
module: `
|
|
package test
|
|
p if {
|
|
some k, x in input with input as [1, 1, 1]
|
|
k == 0
|
|
x == 1
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local1__ = input[__local0__] with input as [1, 1, 1]
|
|
__local0__ = 0
|
|
__local1__ = 1
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
i = 0
|
|
xs = [1, 2]
|
|
k = "foo"
|
|
v = "bar"
|
|
p if {
|
|
every k, v in xs { k + v > i }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
i = 0
|
|
xs = [1, 2]
|
|
k = "foo"
|
|
v = "bar"
|
|
p = true if {
|
|
__local2__ = data.test.xs
|
|
every __local0__, __local1__ in __local2__ {
|
|
plus(__local0__, __local1__, __local3__)
|
|
__local4__ = data.test.i
|
|
gt(__local3__, __local4__)
|
|
}
|
|
} `,
|
|
},
|
|
{
|
|
note: "rewrite every: unused key var",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every k, v in [1] { v >= 1 }
|
|
}
|
|
`,
|
|
wantErr: errors.New("declared var k unused"),
|
|
},
|
|
{
|
|
// NOTE(sr): this would happen when compiling modules twice:
|
|
// the first run rewrites every to include a generated key var,
|
|
// the second one bails because it's not used.
|
|
// Seen in the wild when using `opa test -b` on a bundle that
|
|
// used `every`, https://github.com/open-policy-agent/opa/issues/4420
|
|
note: "rewrite every: unused generated key var",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
every __local0__, v in [1] { v >= 1 }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if {
|
|
__local3__ = [1]
|
|
every __local1__, __local2__ in __local3__ { __local2__ >= 1 }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: unused value var",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every v in [1] { true }
|
|
}
|
|
`,
|
|
wantErr: errors.New("declared var v unused"),
|
|
},
|
|
{
|
|
note: "rewrite every: wildcard value var, used key",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every k, _ in [1] { k >= 0 }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if {
|
|
__local1__ = [1]
|
|
every __local0__, _ in __local1__ { gte(__local0__, 0) }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: wildcard key+value var", // NOTE(sr): may be silly, but valid
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every _, _ in [1] { true }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if { __local0__ = [1]; every _, _ in __local0__ { true } }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: declared vars with different scopes",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
some x
|
|
x = 10
|
|
every x in [1] { x == 1 }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if {
|
|
__local0__ = 10
|
|
__local3__ = [1]
|
|
every __local1__, __local2__ in __local3__ { __local2__ = 1 }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: declared vars used in body",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
some y
|
|
y = 10
|
|
every x in [1] { x == y }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if {
|
|
__local0__ = 10
|
|
__local3__ = [1]
|
|
every __local1__, __local2__ in __local3__ {
|
|
__local2__ = __local0__
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: pops declared var stack",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p contains x if {
|
|
some x
|
|
x = 10
|
|
every _ in [1] { true }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p contains __local0__ if { __local0__ = 10; __local2__ = [1]; every __local1__, _ in __local2__ { true } }
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: nested",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
xs := [[1], [2]]
|
|
every v in [1] {
|
|
every w in xs[v] {
|
|
w == 2
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p = true if {
|
|
__local0__ = [[1], [2]]
|
|
__local5__ = [1]
|
|
every __local1__, __local2__ in __local5__ {
|
|
__local6__ = __local0__[__local2__]
|
|
every __local3__, __local4__ in __local6__ {
|
|
__local4__ = 2
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: with modifier on domain",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every x in input { x == 1 } with input as [1, 1, 1]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local2__ = input with input as [1, 1, 1]
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__ = 1
|
|
} with input as [1, 1, 1]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: with modifier on domain with declared var",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
xs := [1, 2]
|
|
every x in input { x == 1 } with input as xs
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local0__ = [1, 2]
|
|
__local3__ = input with input as __local0__
|
|
every __local1__, __local2__ in __local3__ {
|
|
__local2__ = 1
|
|
} with input as __local0__
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: with modifier on body",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every x in [2] { x == input } with input as 2
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local2__ = [2] with input as 2
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local1__ = input
|
|
} with input as 2
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite every: with modifier on body, using every's key+value",
|
|
module: `
|
|
package test
|
|
# import future.keywords.in
|
|
# import future.keywords.every
|
|
p if {
|
|
every x, y in input { true with data.test.q[x][y] as 100 }
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
p if {
|
|
__local2__ = input
|
|
every __local0__, __local1__ in __local2__ {
|
|
true with data.test.q[__local0__][__local1__] as 100
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite closures",
|
|
module: `
|
|
package test
|
|
x = 1
|
|
y = 2
|
|
p if {
|
|
some x, z
|
|
z = 3
|
|
[x | x = 2; y = 2; some z; z = 4]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
x = 1
|
|
y = 2
|
|
p if {
|
|
__local1__ = 3
|
|
[__local0__ | __local0__ = 2; data.test.y = 2; __local2__ = 4]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite head var",
|
|
module: `
|
|
package test
|
|
x = "a"
|
|
y = 1
|
|
z = 2
|
|
p[x] = [y, z] if {
|
|
some x, z
|
|
x = "b"
|
|
z = 4
|
|
}`,
|
|
exp: `
|
|
package test
|
|
x = "a"
|
|
y = 1
|
|
z = 2
|
|
p[__local0__] = __local2__ if {
|
|
__local0__ = "b"
|
|
__local1__ = 4;
|
|
__local3__ = data.test.y
|
|
__local2__ = [__local3__, __local1__]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rewrite call with root document ref as arg",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
f(input, "bar")
|
|
}
|
|
|
|
f(x, y) if {
|
|
x[y]
|
|
}
|
|
`,
|
|
exp: `
|
|
package test
|
|
|
|
p = true if {
|
|
__local2__ = input;
|
|
data.test.f(__local2__, "bar")
|
|
}
|
|
|
|
f(__local0__, __local1__) = true if {
|
|
__local0__[__local1__]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "redeclare err",
|
|
module: `
|
|
package test
|
|
p if {
|
|
some x
|
|
some x
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x declared above"),
|
|
},
|
|
{
|
|
note: "redeclare err, some/in",
|
|
module: `
|
|
package test
|
|
p if {
|
|
some x
|
|
some i, x in []
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x declared above"),
|
|
},
|
|
{
|
|
note: "redeclare assigned err",
|
|
module: `
|
|
package test
|
|
p if {
|
|
x := 1
|
|
some x
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x assigned above"),
|
|
},
|
|
{
|
|
note: "redeclare assigned err, some/in",
|
|
module: `
|
|
package test
|
|
p if {
|
|
x := 1
|
|
some i, x in []
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x assigned above"),
|
|
},
|
|
{
|
|
note: "redeclare reference err",
|
|
module: `
|
|
package test
|
|
p if {
|
|
data.q[x]
|
|
some x
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x referenced above"),
|
|
},
|
|
{
|
|
note: "redeclare reference err, some/in",
|
|
module: `
|
|
package test
|
|
p if {
|
|
data.q[x]
|
|
some i, x in []
|
|
}
|
|
`,
|
|
wantErr: errors.New("test.rego:5: rego_compile_error: var x referenced above"),
|
|
},
|
|
{
|
|
note: "declare unused err",
|
|
module: `
|
|
package test
|
|
p if {
|
|
some x
|
|
}
|
|
`,
|
|
wantErr: errors.New("declared var x unused"),
|
|
},
|
|
{
|
|
note: "declare unsafe err",
|
|
module: `
|
|
package test
|
|
p contains x if {
|
|
some x
|
|
x == 1
|
|
}
|
|
`,
|
|
wantErr: errors.New("var x is unsafe"),
|
|
},
|
|
{
|
|
note: "declare arg err",
|
|
module: `
|
|
package test
|
|
|
|
f([a]) if {
|
|
some a
|
|
a = 1
|
|
}
|
|
`,
|
|
wantErr: errors.New("arg a redeclared"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
opts := CompileOpts{ParserOptions: ParserOptions{AllFutureKeywords: true}}
|
|
compiler, err := CompileModulesWithOpt(map[string]string{"test.rego": tc.module}, opts)
|
|
if tc.wantErr != nil {
|
|
if err == nil {
|
|
t.Fatal("Expected error but got success")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr.Error()) {
|
|
t.Fatalf("Expected:\n\n%v\n\nbut got:\n\n%v", tc.wantErr, err)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
exp := MustParseModuleWithOpts(tc.exp, opts.ParserOptions)
|
|
result := compiler.Modules["test.rego"]
|
|
if exp.Compare(result) != 0 {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckUnusedFunctionArgVars(t *testing.T) {
|
|
tests := []strictnessTestCase{
|
|
{
|
|
note: "one of the two function args is not used - issue 5602 regression test",
|
|
module: `package test
|
|
func(x, y) if {
|
|
x = 1
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "one of the two ref-head function args is not used",
|
|
module: `package test
|
|
a.b.c.func(x, y) if {
|
|
x = 1
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("a.b.c.func(x, y)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "multiple unused argvar in scope - issue 5602 regression test",
|
|
module: `package test
|
|
func(x, y) if {
|
|
input.baz = 1
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y)"), "", 2, 4),
|
|
Message: "unused argument x. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "some unused argvar in scope - issue 5602 regression test",
|
|
module: `package test
|
|
func(x, y) if {
|
|
input.test == "foo"
|
|
x = 1
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "wildcard argvar that's ignored - issue 5602 regression test",
|
|
module: `package test
|
|
func(x, _) if {
|
|
input.test == "foo"
|
|
x = 1
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "wildcard argvar that's ignored - issue 5602 regression test",
|
|
module: `package test
|
|
func(x, _) if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, _)"), "", 2, 4),
|
|
Message: "unused argument x. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "argvar not used in body but in head - issue 5602 regression test",
|
|
module: `package test
|
|
func(x) := x if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar not used in body but in head value comprehension",
|
|
module: `package test
|
|
a := {"foo": 1}
|
|
func(x) := { x: v | v := a[x] } if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar not used in body but in else-head value comprehension",
|
|
module: `package test
|
|
a := {"foo": 1}
|
|
func(x) if {
|
|
input.test == "foo"
|
|
} else := { x: v | v := a[x] } if {
|
|
input.test == "bar"
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar not used in body and shadowed in head value comprehension",
|
|
module: `package test
|
|
a := {"foo": 1}
|
|
func(x) := { x: v | x := "foo"; v := a[x] } if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x) := { x: v | x := \"foo\"; v := a[x] }"), "", 3, 4),
|
|
Message: "unused argument x. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "argvar used in primary body but not in else body",
|
|
module: `package test
|
|
func(x) if {
|
|
input.test == x
|
|
} else := false if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar used in primary body but not in else body (with wildcard)",
|
|
module: `package test
|
|
func(x, _) if {
|
|
input.test == x
|
|
} else := false if {
|
|
input.test == "foo"
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar not used in primary body but in else body",
|
|
module: `package test
|
|
func(x) if {
|
|
input.test == "foo"
|
|
} else := false if {
|
|
input.test == x
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar not used in primary body but in else body (with wildcard)",
|
|
module: `package test
|
|
func(x, _) if {
|
|
input.test == "foo"
|
|
} else := false if {
|
|
input.test == x
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvar used in primary body but not in implicit else body",
|
|
module: `package test
|
|
func(x) if {
|
|
input.test == x
|
|
} else := false`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvars usage spread over multiple bodies",
|
|
module: `package test
|
|
func(x, y, z) if {
|
|
input.test == x
|
|
} else if {
|
|
input.test == y
|
|
} else if {
|
|
input.test == z
|
|
}`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
{
|
|
note: "argvars usage spread over multiple bodies, missing in first",
|
|
module: `package test
|
|
func(x, y, z) if {
|
|
input.test == "foo"
|
|
} else if {
|
|
input.test == y
|
|
} else if {
|
|
input.test == z
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y, z)"), "", 2, 4),
|
|
Message: "unused argument x. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "argvars usage spread over multiple bodies, missing in second",
|
|
module: `package test
|
|
func(x, y, z) if {
|
|
input.test == x
|
|
} else if {
|
|
input.test == "bar"
|
|
} else if {
|
|
input.test == z
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y, z)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "argvars usage spread over multiple bodies, missing in third",
|
|
module: `package test
|
|
func(x, y, z) if {
|
|
input.test == x
|
|
} else if {
|
|
input.test == y
|
|
} else if {
|
|
input.test == "baz"
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y, z)"), "", 2, 4),
|
|
Message: "unused argument z. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "unused default function argvar",
|
|
module: `package test
|
|
default func(x) := 0`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x) := 0"), "", 2, 12),
|
|
Message: "unused argument x. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Helper()
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler().WithStrict(true)
|
|
compiler.Modules = map[string]*Module{
|
|
"test": module(tc.module),
|
|
}
|
|
compileStages(compiler, "")
|
|
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompileUnusedAssignedVarsErrorLocations(t *testing.T) {
|
|
tests := []strictnessTestCase{
|
|
{
|
|
note: "one of the two function args is not used - issue 5662 regression test",
|
|
module: `package test
|
|
func(x, y) if {
|
|
x = 1
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("func(x, y)"), "", 2, 4),
|
|
Message: "unused argument y. (hint: use _ (wildcard variable) instead)",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "multiple unused assigned var in scope - issue 5662 regression test",
|
|
module: `package test
|
|
allow if {
|
|
input.message == "world"
|
|
input.test == "foo"
|
|
input.x == "foo"
|
|
input.y == "baz"
|
|
a := 1
|
|
b := 2
|
|
x := {
|
|
"a": a,
|
|
"b": "bar",
|
|
}
|
|
input.z == "baz"
|
|
c := 3
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("b := 2"), "", 8, 5),
|
|
Message: "assigned var b unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("x := {\n\t\t\t\t\t\"a\": a,\n\t\t\t\t\t\"b\": \"bar\",\n\t\t\t\t}"), "", 9, 5),
|
|
Message: "assigned var x unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("c := 3"), "", 14, 5),
|
|
Message: "assigned var c unused",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
t.Helper()
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler().WithStrict(true)
|
|
compiler.Modules = map[string]*Module{
|
|
"test": module(tc.module),
|
|
}
|
|
compileStages(compiler, "")
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, true)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompileUnusedDeclaredVarsErrorLocations(t *testing.T) {
|
|
tests := []strictnessTestCase{
|
|
{
|
|
note: "simple unused some var - issue 4238 regression test",
|
|
module: `package test
|
|
|
|
foo if {
|
|
print("Hello world")
|
|
some i
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some i"), "", 5, 5),
|
|
Message: "declared var i unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "simple unused some vars, 2x rules",
|
|
module: `package test
|
|
|
|
foo if {
|
|
print("Hello world")
|
|
some i
|
|
}
|
|
|
|
bar if {
|
|
print("Hello world")
|
|
some j
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some i"), "", 5, 5),
|
|
Message: "declared var i unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some j"), "", 10, 5),
|
|
Message: "declared var j unused",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "multiple unused some vars",
|
|
module: `package test
|
|
|
|
x := [1, 1, 1]
|
|
foo2 if {
|
|
print("A")
|
|
some a, b, c
|
|
some i, j
|
|
some k
|
|
x[b] == 1
|
|
print("B")
|
|
}`,
|
|
expectedErrors: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some a, b, c"), "", 6, 5),
|
|
Message: "declared var a unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some a, b, c"), "", 6, 5),
|
|
Message: "declared var c unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some i, j"), "", 7, 5),
|
|
Message: "declared var i unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some i, j"), "", 7, 5),
|
|
Message: "declared var j unused",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Location: NewLocation([]byte("some k"), "", 8, 5),
|
|
Message: "declared var k unused",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// This is similar to the logic for runStrictnessTestCase(), but expects
|
|
// unconditional compiler errors.
|
|
t.Helper()
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler().WithStrict(true)
|
|
compiler.Modules = map[string]*Module{
|
|
"test": module(tc.module),
|
|
}
|
|
compileStages(compiler, "")
|
|
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, true)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompileInvalidEqAssignExpr(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion RegoVersion
|
|
}{
|
|
{
|
|
note: "v0",
|
|
regoVersion: RegoV0,
|
|
},
|
|
{
|
|
note: "v1",
|
|
regoVersion: RegoV1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
|
|
c.Modules["error"] = MustParseModuleWithOpts(`package errors
|
|
|
|
p if {
|
|
# Arity mismatches are caught in the checkUndefinedFuncs check,
|
|
# and invalid eq/assign calls are passed along until then.
|
|
assign()
|
|
assign(1)
|
|
eq()
|
|
eq(1)
|
|
}`, ParserOptions{RegoVersion: tc.regoVersion, AllFutureKeywords: true})
|
|
|
|
// Run up to CheckRuleConflicts (the stage before CheckUndefinedFuncs)
|
|
compileStages(c, StageCheckRuleConflicts)
|
|
assertNotFailed(t, c)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteComprehensionTerm(t *testing.T) {
|
|
|
|
c := NewCompiler()
|
|
c.Modules["head"] = MustParseModule(`package head
|
|
arr = [[1], [2], [3]]
|
|
arr2 = [["a"], ["b"], ["c"]]
|
|
arr_comp = [[x[i]] | arr[j] = x]
|
|
set_comp = {[x[i]] | arr[j] = x}
|
|
obj_comp = {x[i]: x[i] | arr2[j] = x}
|
|
`)
|
|
|
|
compileStages(c, StageRewriteComprehensionTerms)
|
|
assertNotFailed(t, c)
|
|
|
|
arrCompRule := c.Modules["head"].Rules[2]
|
|
exp1 := MustParseRule(`arr_comp = [__local0__ | data.head.arr[j] = x; __local0__ = [x[i]]] { true }`)
|
|
assertRulesEqual(t, arrCompRule, exp1)
|
|
|
|
setCompRule := c.Modules["head"].Rules[3]
|
|
exp2 := MustParseRule(`set_comp = {__local1__ | data.head.arr[j] = x; __local1__ = [x[i]]} { true }`)
|
|
assertRulesEqual(t, setCompRule, exp2)
|
|
|
|
objCompRule := c.Modules["head"].Rules[4]
|
|
exp3 := MustParseRule(`obj_comp = {__local2__: __local3__ | data.head.arr2[j] = x; __local2__ = x[i]; __local3__ = x[i]} { true }`)
|
|
assertRulesEqual(t, objCompRule, exp3)
|
|
}
|
|
|
|
func TestCompilerRewriteDoubleEq(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
input string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "vars and constants",
|
|
input: "p if { x = 1; x == 1; y = [1,2,3]; y == [1,2,3] }",
|
|
exp: `x = 1; x = 1; y = [1,2,3]; y = [1,2,3]`,
|
|
},
|
|
{
|
|
note: "refs",
|
|
input: "p if { input.x == data.y }",
|
|
exp: `input.x = data.y`,
|
|
},
|
|
{
|
|
note: "comprehensions",
|
|
input: "p if { [1|true] == [2|true] }",
|
|
exp: `[1|true] = [2|true]`,
|
|
},
|
|
// TODO(tsandall): improve support for calls so that extra unification step is
|
|
// not required. This requires more changes to the compiler as the initial
|
|
// stages that rewrite term exprs needs to be updated to handle == differently
|
|
// and then other stages need to be reviewed to make sure they can deal with
|
|
// nested calls. Alternatively, the compiler could keep track of == exprs that
|
|
// have been converted into = and then the safety check would need to be updated.
|
|
{
|
|
note: "calls",
|
|
input: "p if { count([1,2]) == 2 }",
|
|
exp: `count([1,2], __local0__); __local0__ = 2`,
|
|
},
|
|
{
|
|
note: "embedded",
|
|
input: "p if { x = 1; y = [x == 0] }",
|
|
exp: `x = 1; equal(x, 0, __local0__); y = [__local0__]`,
|
|
},
|
|
{
|
|
note: "embedded in call",
|
|
input: `p if { x = 0; neq(true, x == 1) }`,
|
|
exp: `x = 0; equal(x, 1, __local0__); neq(true, __local0__)`,
|
|
},
|
|
{
|
|
note: "comprehension in object key",
|
|
input: `p if { {{1 | 0 == 0}: 2} }`,
|
|
exp: `{{1 | 0 = 0}: 2}`,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules["test"] = module("package test\n" + tc.input)
|
|
compileStages(c, StageRewriteEquals)
|
|
assertNotFailed(t, c)
|
|
exp := MustParseBody(tc.exp)
|
|
result := c.Modules["test"].Rules[0].Body
|
|
if result.Compare(exp) != 0 {
|
|
t.Fatalf("\nExp: %v\nGot: %v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteDynamicTerms(t *testing.T) {
|
|
|
|
fixture := `
|
|
package test
|
|
str = "hello"
|
|
`
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{`arr if { [str] }`, `__local0__ = data.test.str; [__local0__]`},
|
|
{`arr2 if { [[str]] }`, `__local0__ = data.test.str; [[__local0__]]`},
|
|
{`obj if { {"x": str} }`, `__local0__ = data.test.str; {"x": __local0__}`},
|
|
{`obj2 if { {"x": {"y": str}} }`, `__local0__ = data.test.str; {"x": {"y": __local0__}}`},
|
|
{`set if { {str} }`, `__local0__ = data.test.str; {__local0__}`},
|
|
{`set2 if { {{str}} }`, `__local0__ = data.test.str; {{__local0__}}`},
|
|
{`ref if { str[str] }`, `__local0__ = data.test.str; data.test.str[__local0__]`},
|
|
{`ref2 if { str[str[str]] }`, `__local0__ = data.test.str; __local1__ = data.test.str[__local0__]; data.test.str[__local1__]`},
|
|
{`arr_compr if { [1 | [str]] }`, `[1 | __local0__ = data.test.str; [__local0__]]`},
|
|
{`arr_compr2 if { [1 | [1 | [str]]] }`, `[1 | [1 | __local0__ = data.test.str; [__local0__]]]`},
|
|
{`set_compr if { {1 | [str]} }`, `{1 | __local0__ = data.test.str; [__local0__]}`},
|
|
{`set_compr2 if { {1 | {1 | [str]}} }`, `{1 | {1 | __local0__ = data.test.str; [__local0__]}}`},
|
|
{`obj_compr if { {"a": "b" | [str]} }`, `{"a": "b" | __local0__ = data.test.str; [__local0__]}`},
|
|
{`obj_compr2 if { {"a": "b" | {"a": "b" | [str]}} }`, `{"a": "b" | {"a": "b" | __local0__ = data.test.str; [__local0__]}}`},
|
|
{`equality if { str = str }`, `data.test.str = data.test.str`},
|
|
{`equality2 if { [str] = [str] }`, `__local0__ = data.test.str; __local1__ = data.test.str; [__local0__] = [__local1__]`},
|
|
{`call if { startswith(str, "") }`, `__local0__ = data.test.str; startswith(__local0__, "")`},
|
|
{`call2 if { count([str], n) }`, `__local0__ = data.test.str; count([__local0__], n)`},
|
|
{`eq_with if { [str] = [1] with input as 1 }`, `__local0__ = data.test.str with input as 1; [__local0__] = [1] with input as 1`},
|
|
{`term_with if { [[str]] with input as 1 }`, `__local0__ = data.test.str with input as 1; [[__local0__]] with input as 1`},
|
|
{`call_with if { count(str) with input as 1 }`, `__local0__ = data.test.str with input as 1; count(__local0__) with input as 1`},
|
|
{`call_func if { f(input, "foo") } f(x,y) if { x[y] }`, `__local2__ = input; data.test.f(__local2__, "foo")`},
|
|
{`call_func2 if { f(input.foo, "foo") } f(x,y) if { x[y] }`, `__local2__ = input.foo; data.test.f(__local2__, "foo")`},
|
|
{`every_domain if { every _ in str { true } }`, `__local1__ = data.test.str; every __local0__, _ in __local1__ { true }`},
|
|
{`every_domain_array if { every _ in [1, 2, 3] { true } }`, `__local1__ = [1, 2, 3]; every __local0__, _ in __local1__ { true }`},
|
|
{`every_domain_call if { every _ in numbers.range(1, 10) { true } }`, `numbers.range(1, 10, __local2__); __local1__ = __local2__; every __local0__, _ in __local1__ { true }`},
|
|
{`every_domain_array_w_calls if { every _ in [1 / 2, "foo", abs(-1)] { true } }`, `div(1, 2, __local2__); abs(-1, __local3__); __local1__ = [__local2__, "foo", __local3__]; every __local0__, _ in __local1__ { true }`},
|
|
{`every_body if { every _ in [] { [str] } }`,
|
|
`__local1__ = []; every __local0__, _ in __local1__ { __local2__ = data.test.str; [__local2__] }`},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
c.Modules["test"] = module(fixture + tc.input)
|
|
compileStages(c, StageRewriteDynamicTerms)
|
|
assertNotFailed(t, c)
|
|
expected := MustParseBodyWithOpts(tc.expected, opts)
|
|
result := c.Modules["test"].Rules[1].Body
|
|
if result.Compare(expected) != 0 {
|
|
t.Fatalf("\nExp: %v\nGot: %v", expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteWithValue(t *testing.T) {
|
|
fixture := `package test
|
|
|
|
arr = ["hello", "goodbye"]
|
|
|
|
`
|
|
|
|
tests := []struct {
|
|
note string
|
|
input string
|
|
opts func(*Compiler) *Compiler
|
|
expected string
|
|
expectedRule *Rule
|
|
wantErr error
|
|
}{
|
|
{
|
|
note: "nop",
|
|
input: `p if { true with input as 1 }`,
|
|
expected: `p if { true with input as 1 }`,
|
|
},
|
|
{
|
|
note: "refs",
|
|
input: `p if { true with input as arr }`,
|
|
expected: `p if { __local0__ = data.test.arr; true with input as __local0__ }`,
|
|
},
|
|
{
|
|
note: "array comprehension",
|
|
input: `p if { true with input as [true | true] }`,
|
|
expected: `p if { __local0__ = [true | true]; true with input as __local0__ }`,
|
|
},
|
|
{
|
|
note: "set comprehension",
|
|
input: `p if { true with input as {true | true} }`,
|
|
expected: `p if { __local0__ = {true | true}; true with input as __local0__ }`,
|
|
},
|
|
{
|
|
note: "object comprehension",
|
|
input: `p if { true with input as {"k": true | true} }`,
|
|
expected: `p if { __local0__ = {"k": true | true}; true with input as __local0__ }`,
|
|
},
|
|
{
|
|
note: "comprehension nested",
|
|
input: `p if { true with input as [true | true with input as arr] }`,
|
|
expected: `p if { __local0__ = [true | __local1__ = data.test.arr; true with input as __local1__]; true with input as __local0__ }`,
|
|
},
|
|
{
|
|
note: "multiple",
|
|
input: `p if { true with input.a as arr[0] with input.b as arr[1] }`,
|
|
expected: `p if { __local0__ = data.test.arr[0]; __local1__ = data.test.arr[1]; true with input.a as __local0__ with input.b as __local1__ }`,
|
|
},
|
|
{
|
|
note: "invalid target",
|
|
input: `p if { true with foo.q as 1 }`,
|
|
wantErr: errors.New("rego_type_error: with keyword target must reference existing input, data, or a function"),
|
|
},
|
|
{
|
|
note: "built-in function: replaced by (unknown) var",
|
|
input: `p if { true with time.now_ns as foo }`,
|
|
expected: `p if { true with time.now_ns as foo }`, // `foo` still a Var here
|
|
},
|
|
{
|
|
note: "built-in function: valid, arity 0",
|
|
input: `
|
|
p if { true with time.now_ns as now }
|
|
now() = 1
|
|
`,
|
|
expected: `p if { true with time.now_ns as data.test.now }`,
|
|
},
|
|
{
|
|
note: "built-in function: valid func ref, arity 1",
|
|
input: `
|
|
p if { true with http.send as mock_http_send }
|
|
mock_http_send(_) = { "body": "yay" }
|
|
`,
|
|
expected: `p if { true with http.send as data.test.mock_http_send }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by value",
|
|
input: `
|
|
p if { true with http.send as { "body": "yay" } }
|
|
`,
|
|
expected: `p if { true with http.send as {"body": "yay"} }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by var",
|
|
input: `
|
|
p if {
|
|
resp := { "body": "yay" }
|
|
true with http.send as resp
|
|
}
|
|
`,
|
|
expected: `p if { __local0__ = {"body": "yay"}; true with http.send as __local0__ }`,
|
|
},
|
|
{
|
|
note: "non-built-in function: replaced by var",
|
|
input: `
|
|
p if {
|
|
resp := true
|
|
f(true) with f as resp
|
|
}
|
|
f(false) if { true }
|
|
`,
|
|
expected: `p if { __local0__ = true; data.test.f(true) with data.test.f as __local0__ }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by comprehension",
|
|
input: `
|
|
p if { true with http.send as { x: true | x := ["a", "b"][_] } }
|
|
`,
|
|
expected: `p if { __local2__ = {__local0__: true | __local1__ = ["a", "b"]; __local0__ = __local1__[_]}; true with http.send as __local2__ }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by ref",
|
|
input: `
|
|
p if { true with http.send as resp }
|
|
resp := { "body": "yay" }
|
|
`,
|
|
expected: `p if { true with http.send as data.test.resp }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by another built-in (ref)",
|
|
input: `
|
|
p if { true with http.send as object.union_n }
|
|
`,
|
|
expected: `p if { true with http.send as object.union_n }`,
|
|
},
|
|
{
|
|
note: "built-in function: replaced by another built-in (simple)",
|
|
input: `
|
|
p if { true with http.send as count }
|
|
`,
|
|
expectedRule: func() *Rule {
|
|
r := MustParseRule(`p { true with http.send as count }`)
|
|
r.Body[0].With[0].Value.Value = Ref([]*Term{VarTerm("count")})
|
|
return r
|
|
}(),
|
|
},
|
|
{
|
|
note: "built-in function: replaced by another built-in that's marked unsafe",
|
|
input: `
|
|
q := is_object({"url": "https://httpbin.org", "method": "GET"})
|
|
p if { q with is_object as http.send }
|
|
`,
|
|
opts: func(c *Compiler) *Compiler { return c.WithUnsafeBuiltins(map[string]struct{}{"http.send": {}}) },
|
|
wantErr: errors.New("rego_compile_error: with keyword replacing built-in function: target must not be unsafe: \"http.send\""),
|
|
},
|
|
{
|
|
note: "non-built-in function: replaced by another built-in that's marked unsafe",
|
|
input: `
|
|
r(_) = {}
|
|
q := r({"url": "https://httpbin.org", "method": "GET"})
|
|
p if {
|
|
q with r as http.send
|
|
}`,
|
|
opts: func(c *Compiler) *Compiler { return c.WithUnsafeBuiltins(map[string]struct{}{"http.send": {}}) },
|
|
wantErr: errors.New("rego_compile_error: with keyword replacing built-in function: target must not be unsafe: \"http.send\""),
|
|
},
|
|
{
|
|
note: "built-in function: valid, arity 1, non-compound name",
|
|
input: `
|
|
p if { concat("/", input) with concat as mock_concat }
|
|
mock_concat(_, _) = "foo/bar"
|
|
`,
|
|
expectedRule: func() *Rule {
|
|
r := MustParseRuleWithOpts(`p if { concat("/", input) with concat as data.test.mock_concat }`,
|
|
ParserOptions{RegoVersion: RegoV1})
|
|
r.Body[0].With[0].Target.Value = Ref([]*Term{VarTerm("concat")})
|
|
return r
|
|
}(),
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
if tc.opts != nil {
|
|
c = tc.opts(c)
|
|
}
|
|
c.Modules["test"] = module(fixture + tc.input)
|
|
compileStages(c, StageRewriteWithValues)
|
|
if tc.wantErr == nil {
|
|
assertNotFailed(t, c)
|
|
expected := tc.expectedRule
|
|
if expected == nil {
|
|
expected = MustParseRuleWithOpts(tc.expected, ParserOptions{RegoVersion: RegoV1})
|
|
}
|
|
result := c.Modules["test"].Rules[1]
|
|
if result.Compare(expected) != 0 {
|
|
t.Fatalf("\nExp: %v\nGot: %v", expected, result)
|
|
}
|
|
} else {
|
|
assertCompilerErrorStrings(t, c, []string{tc.wantErr.Error()})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewritePrintCallsErasure(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "no-op",
|
|
module: `package test
|
|
p if { true }`,
|
|
exp: `package test
|
|
p if { true }`,
|
|
},
|
|
{
|
|
note: "replace empty body with true",
|
|
module: `package test
|
|
|
|
p if { print(1) }
|
|
`,
|
|
exp: `package test
|
|
|
|
p if { true } `,
|
|
},
|
|
{
|
|
note: "rule body",
|
|
module: `package test
|
|
|
|
p if { false; print(1) }
|
|
`,
|
|
exp: `package test
|
|
|
|
p if { false } `,
|
|
},
|
|
{
|
|
note: "set comprehension body",
|
|
module: `package test
|
|
|
|
p if { {1 | false; print(1)} }
|
|
`,
|
|
exp: `package test
|
|
|
|
p if { {1 | false} } `,
|
|
},
|
|
{
|
|
note: "array comprehension body",
|
|
module: `package test
|
|
|
|
p if { [1 | false; print(1)] }
|
|
`,
|
|
exp: `package test
|
|
|
|
p if { [1 | false] } `,
|
|
},
|
|
{
|
|
note: "object comprehension body",
|
|
module: `package test
|
|
|
|
p if { {"x": 1 | false; print(1)} }
|
|
`,
|
|
exp: `package test
|
|
|
|
p if { {"x": 1 | false} } `,
|
|
},
|
|
{
|
|
note: "every body",
|
|
module: `package test
|
|
|
|
p if { every _ in [] { false; print(1) } }
|
|
`,
|
|
exp: `package test
|
|
|
|
p = true if { __local1__ = []; every __local0__, _ in __local1__ { false } }`,
|
|
},
|
|
{
|
|
note: "in head",
|
|
module: `package test
|
|
|
|
p = {1 | print("x")}`,
|
|
exp: `package test
|
|
|
|
p = __local0__ if { __local0__ = {1 | true} }`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithEnablePrintStatements(false)
|
|
c.Compile(map[string]*Module{"test.rego": module(tc.module)})
|
|
assertNotFailed(t, c)
|
|
|
|
if exp := module(tc.exp); !exp.Equal(c.Modules["test.rego"]) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, c.Modules["test.rego"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewritePrintCallsErrors(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
exp error
|
|
errCode string
|
|
}{
|
|
{
|
|
note: "non-existent var",
|
|
module: `package test
|
|
|
|
p if { print(x) }`,
|
|
exp: errors.New("var x is undeclared"),
|
|
errCode: CompileErr,
|
|
},
|
|
{
|
|
note: "declared after print",
|
|
module: `package test
|
|
|
|
p if { print(x); x = 7 }`,
|
|
exp: errors.New("var x is undeclared"),
|
|
errCode: CompileErr,
|
|
},
|
|
{
|
|
note: "inside comprehension",
|
|
module: `package test
|
|
p if { {1 | print(x)} = {1 | print(7)} }
|
|
`,
|
|
exp: errors.New("var x is undeclared"),
|
|
errCode: CompileErr,
|
|
},
|
|
{
|
|
note: "inside template-string",
|
|
module: `package test
|
|
p if { $"<{print(42)}>" }
|
|
`,
|
|
exp: errors.New("print(42) used as value"),
|
|
errCode: TypeErr,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithEnablePrintStatements(true)
|
|
c.Compile(map[string]*Module{
|
|
"test.rego": module(tc.module),
|
|
})
|
|
if !c.Failed() {
|
|
t.Fatal("expected error")
|
|
}
|
|
if c.Errors[0].Code != tc.errCode || c.Errors[0].Message != tc.exp.Error() {
|
|
t.Fatal("unexpected error:", c.Errors)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Regression test for bug #7647
|
|
// head values in nested comprehensions should not lead to undeclared error.
|
|
func TestCompilterRewritePrintCallsNestedComprehensionLocalsSafe(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
}{
|
|
{
|
|
note: "print variable from nested comprehension without error",
|
|
module: `package test
|
|
f(_) := {"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}
|
|
|
|
p := [v |
|
|
m := {l | l := f(true)[k]}[_]
|
|
v := m[_]
|
|
print(v)
|
|
]`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithEnablePrintStatements(true)
|
|
c.Compile(map[string]*Module{"test.rego": module(tc.module)})
|
|
assertNotFailed(t, c)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewritePrintCalls(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "print one",
|
|
module: `package test
|
|
|
|
p if { print(1) }`,
|
|
exp: `package test
|
|
|
|
p = true if { __local1__ = {__local0__ | __local0__ = 1}; internal.print([__local1__]) }`,
|
|
},
|
|
{
|
|
note: "print multiple",
|
|
module: `package test
|
|
|
|
p if { print(1, 2) }`,
|
|
exp: `package test
|
|
|
|
p = true if { __local2__ = {__local0__ | __local0__ = 1}; __local3__ = {__local1__ | __local1__ = 2}; internal.print([__local2__, __local3__]) }`,
|
|
},
|
|
{
|
|
note: "print inside set comprehension",
|
|
module: `package test
|
|
|
|
p if { x = 1; {2 | print(x)} }`,
|
|
exp: `package test
|
|
|
|
p = true if { x = 1; {2 | __local1__ = {__local0__ | __local0__ = x}; internal.print([__local1__])} }`,
|
|
},
|
|
{
|
|
note: "print inside array comprehension",
|
|
module: `package test
|
|
|
|
p if { x = 1; [2 | print(x)] }`,
|
|
exp: `package test
|
|
|
|
p = true if { x = 1; [2 | __local1__ = {__local0__ | __local0__ = x}; internal.print([__local1__])] }`,
|
|
},
|
|
{
|
|
note: "print inside object comprehension",
|
|
module: `package test
|
|
|
|
p if { x = 1; {"x": 2 | print(x)} }`,
|
|
exp: `package test
|
|
|
|
p = true if { x = 1; {"x": 2 | __local1__ = {__local0__ | __local0__ = x}; internal.print([__local1__])} }`,
|
|
},
|
|
{
|
|
note: "print inside every",
|
|
module: `package test
|
|
|
|
p if { every x in [1,2] { print(x) } }`,
|
|
exp: `package test
|
|
|
|
p = true if {
|
|
__local3__ = [1, 2]
|
|
every __local0__, __local1__ in __local3__ {
|
|
__local4__ = {__local2__ | __local2__ = __local1__}
|
|
internal.print([__local4__])
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
note: "print output of nested call",
|
|
module: `package test
|
|
|
|
p if {
|
|
x := split("abc", "")[y]
|
|
print(x, y)
|
|
}`,
|
|
exp: `package test
|
|
|
|
p = true if { split("abc", "", __local3__); __local0__ = __local3__[y]; __local4__ = {__local1__ | __local1__ = __local0__}; __local5__ = {__local2__ | __local2__ = y}; internal.print([__local4__, __local5__]) }`,
|
|
},
|
|
{
|
|
note: "print call in head",
|
|
module: `package test
|
|
|
|
p = {1 | print("x") }`,
|
|
exp: `package test
|
|
|
|
p = __local1__ if {
|
|
__local1__ = {1 | __local2__ = { __local0__ | __local0__ = "x"}; internal.print([__local2__])}
|
|
}`,
|
|
},
|
|
{
|
|
note: "print call in head - args treated as safe",
|
|
module: `package test
|
|
|
|
f(a) = {1 | a[x]; print(x)}`,
|
|
exp: `package test
|
|
|
|
f(__local0__) = __local2__ if { __local2__ = {1 | __local0__[x]; __local3__ = {__local1__ | __local1__ = x}; internal.print([__local3__])} }
|
|
`,
|
|
},
|
|
{
|
|
note: "print call of var in head key",
|
|
module: `package test
|
|
f(_) = [1, 2, 3]
|
|
p contains x if { [_, x, _] := f(true); print(x) }`,
|
|
exp: `package test
|
|
f(__local0__) = [1, 2, 3] if { true }
|
|
p contains __local2__ if { data.test.f(true, __local5__); [__local1__, __local2__, __local3__] = __local5__; __local6__ = {__local4__ | __local4__ = __local2__}; internal.print([__local6__]) }
|
|
`,
|
|
},
|
|
{
|
|
note: "print call of var in head value",
|
|
module: `package test
|
|
f(_) = [1, 2, 3]
|
|
p = x if { [_, x, _] := f(true); print(x) }`,
|
|
exp: `package test
|
|
f(__local0__) = [1, 2, 3] if { true }
|
|
p = __local2__ if { data.test.f(true, __local5__); [__local1__, __local2__, __local3__] = __local5__; __local6__ = {__local4__ | __local4__ = __local2__}; internal.print([__local6__]) }
|
|
`,
|
|
},
|
|
{
|
|
note: "print call of vars in head key and value",
|
|
module: `package test
|
|
f(_) = [1, 2, 3]
|
|
p[x] = y if { [_, x, y] := f(true); print(x) }`,
|
|
exp: `package test
|
|
f(__local0__) = [1, 2, 3] if { true }
|
|
p[__local2__] = __local3__ if { data.test.f(true, __local5__); [__local1__, __local2__, __local3__] = __local5__; __local6__ = {__local4__ | __local4__ = __local2__}; internal.print([__local6__]) }
|
|
`,
|
|
},
|
|
{
|
|
note: "print call of vars altered with 'with' and call",
|
|
module: `package test
|
|
q = input
|
|
p if {
|
|
x := q with input as json.unmarshal("{}")
|
|
print(x)
|
|
}`,
|
|
exp: `package test
|
|
q = __local3__ if { __local3__ = input }
|
|
p = true if {
|
|
json.unmarshal("{}", __local2__)
|
|
__local0__ = data.test.q with input as __local2__
|
|
__local4__ = {__local1__ | __local1__ = __local0__}
|
|
internal.print([__local4__])
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithEnablePrintStatements(true)
|
|
c.Compile(map[string]*Module{"test.rego": module(tc.module)})
|
|
assertNotFailed(t, c)
|
|
|
|
if exp := module(tc.exp); !exp.Equal(c.Modules["test.rego"]) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, c.Modules["test.rego"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerRewriteTemplateStrings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type rewriteTest struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}
|
|
|
|
caps := CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true))
|
|
|
|
opts := CompileOpts{
|
|
ParserOptions: ParserOptions{
|
|
Capabilities: caps,
|
|
},
|
|
}
|
|
|
|
popts := func(options ParserOptions) ParserOptions {
|
|
options.Capabilities = caps
|
|
options.AllFutureKeywords = true
|
|
return options
|
|
}
|
|
|
|
cases := func(rewriteCases []rewriteTest) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Helper()
|
|
for _, tc := range rewriteCases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
t.Parallel()
|
|
t.Helper()
|
|
c := MustCompileModulesWithOpts(map[string]string{"test.rego": tc.module}, opts)
|
|
if exp, act := module(tc.exp, popts), c.Modules["test.rego"]; !exp.Equal(act) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, act)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Run("empty template string", cases([]rewriteTest{{
|
|
note: "empty template string, head value",
|
|
module: `package test
|
|
p := $""`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
internal.template_string([""], __local0__)
|
|
}`,
|
|
}, {
|
|
note: "empty template string, head set value",
|
|
module: `package test
|
|
p contains $""`,
|
|
exp: `package test
|
|
p contains __local0__ if {
|
|
internal.template_string([""], __local0__)
|
|
}`,
|
|
}, {
|
|
note: "empty template string, head map key",
|
|
module: `package test
|
|
p[$""] := true`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
internal.template_string([""], __local1__)
|
|
__local0__ = __local1__
|
|
}`,
|
|
}, {
|
|
note: "empty template string, in body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $""
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
internal.template_string([""], __local1__)
|
|
__local0__ = __local1__
|
|
}`,
|
|
}, {
|
|
note: "empty template string, in body, no output arg",
|
|
module: `package test
|
|
p if {
|
|
$""
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
internal.template_string([""], __local0__)
|
|
__local0__
|
|
}`,
|
|
},
|
|
}))
|
|
|
|
t.Run("no template expressions", cases([]rewriteTest{{
|
|
note: "non-empty template string, no template expression, head value",
|
|
module: `package test
|
|
p := $"foo bar"`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
internal.template_string(["foo bar"], __local0__)
|
|
}`,
|
|
}, {
|
|
note: "non-empty template string, no template expression, head set value",
|
|
module: `package test
|
|
p contains $"foo bar"`,
|
|
exp: `package test
|
|
p contains __local0__ if {
|
|
internal.template_string(["foo bar"], __local0__)
|
|
}`,
|
|
}, {
|
|
note: "non-empty template string, no template expression, head map key",
|
|
module: `package test
|
|
p[$"foo bar"] := true`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
internal.template_string(["foo bar"], __local1__)
|
|
__local0__ = __local1__
|
|
}`,
|
|
}, {
|
|
note: "non-empty template string, no template expression, in body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"foo bar"
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
internal.template_string(["foo bar"], __local1__)
|
|
__local0__ = __local1__
|
|
}`,
|
|
}, {
|
|
note: "non-empty template string, no template expression, in body, no output arg",
|
|
module: `package test
|
|
p if {
|
|
$"foo bar"
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
internal.template_string(["foo bar"], __local0__)
|
|
__local0__
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("ref template expression", cases([]rewriteTest{{
|
|
note: "single template expression, ref, head value",
|
|
module: `package test
|
|
p := $"{input.x}"`,
|
|
exp: `package test
|
|
p := __local1__ if {
|
|
__local2__ = {__local0__ | __local0__ = input.x}; internal.template_string([__local2__], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, ref, head set value",
|
|
module: `package test
|
|
p contains $"{input.x}"`,
|
|
exp: `package test
|
|
p contains __local1__ if {
|
|
__local2__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local2__], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, ref, head map key",
|
|
module: `package test
|
|
p[$"{input.x}"] := true`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
__local3__ = {__local1__ | __local1__ = input.x}
|
|
internal.template_string([__local3__], __local2__)
|
|
__local0__ = __local2__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, ref, in body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"{input.x}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
__local3__ = {__local1__ | __local1__ = input.x}
|
|
internal.template_string([__local3__], __local2__)
|
|
__local0__ = __local2__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, ref, in body, no output arg",
|
|
module: `package test
|
|
p if {
|
|
$"{input.x}"
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local2__ = {__local0__ |
|
|
__local0__ = input.x
|
|
}
|
|
internal.template_string([__local2__], __local1__)
|
|
__local1__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, ref, in function arg",
|
|
module: `package test
|
|
f($"{input.x}") := 42`,
|
|
exp: `package test
|
|
f(__local1__) := 42 if {
|
|
__local2__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local2__], __local1__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("var template expression", cases([]rewriteTest{{
|
|
note: "single template expression, var, head value",
|
|
module: `package test
|
|
p := $"{x}" if {
|
|
x := 42
|
|
}`,
|
|
exp: `package test
|
|
p := __local1__ if {
|
|
__local0__ = 42
|
|
internal.template_string([{__local0__}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var, indirection, head value",
|
|
module: `package test
|
|
p := $"{x}" if {
|
|
x := input.x
|
|
}`,
|
|
exp: `package test
|
|
p := __local1__ if {
|
|
__local0__ = input.x
|
|
internal.template_string([{__local0__}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var, head set value",
|
|
module: `package test
|
|
p contains $"{x}" if {
|
|
x := 42
|
|
}`,
|
|
exp: `package test
|
|
p contains __local1__ if {
|
|
__local0__ = 42
|
|
internal.template_string([{__local0__}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var, head map key",
|
|
module: `package test
|
|
p[$"{x}"] := true if {
|
|
x := 42
|
|
}`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
__local1__ = 42
|
|
internal.template_string([{__local1__}], __local2__)
|
|
__local0__ = __local2__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var, in body",
|
|
module: `package test
|
|
p := y if {
|
|
x := 42
|
|
y := $"{x}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local1__ if {
|
|
__local0__ = 42
|
|
internal.template_string([{__local0__}], __local2__)
|
|
__local1__ = __local2__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var, in body, no output arg",
|
|
module: `package test
|
|
p if {
|
|
x := 42
|
|
$"{x}"
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local0__ = 42
|
|
internal.template_string([{__local0__}], __local1__)
|
|
__local1__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var from function args, in head args",
|
|
module: `package test
|
|
f($"{x}") := 42 if {
|
|
x := "foo"
|
|
}`,
|
|
exp: `package test
|
|
f(__local1__) := 42 if {
|
|
__local0__ = "foo"
|
|
internal.template_string([{__local0__}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var from function args, in head value",
|
|
module: `package test
|
|
f(x) := $"{x}"`,
|
|
exp: `package test
|
|
f(__local0__) := __local1__ if {
|
|
internal.template_string([{__local0__}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, var from function args, in body",
|
|
module: `package test
|
|
f(x) := y if {
|
|
y := $"{x}"
|
|
}`,
|
|
exp: `package test
|
|
f(__local0__) := __local1__ if {
|
|
internal.template_string([{__local0__}], __local2__)
|
|
__local1__ = __local2__
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("primitives", cases([]rewriteTest{{
|
|
note: "primitives",
|
|
module: `package test
|
|
p := $"{false}, {42}, {13.37}, {"foo"}, {` + "`bar`" + `}, {null}"`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
internal.template_string([false, ", ", 42, ", ", 13.37, ", ", "foo", ", ", "bar", ", ", null], __local0__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("collections", cases([]rewriteTest{{
|
|
note: "collections",
|
|
module: `package test
|
|
p := $"{[1, 2, 3]}, {{false, true}}, {{"a": "b"}}"`,
|
|
exp: `package test
|
|
p := __local3__ if {
|
|
__local4__ = {__local0__ | __local0__ = [1, 2, 3]}
|
|
__local5__ = {__local1__ | __local1__ = {false, true}}
|
|
__local6__ = {__local2__ | __local2__ = {"a": "b"}}
|
|
internal.template_string([__local4__, ", ", __local5__, ", ", __local6__], __local3__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("call template-expression", cases([]rewriteTest{{
|
|
note: "single template expression, call, head value",
|
|
module: `package test
|
|
f(x) := x
|
|
p := $"{f(input.x)}"`,
|
|
exp: `package test
|
|
f(__local0__) := __local0__ if { true }
|
|
p := __local3__ if {
|
|
__local5__ = {__local1__ |
|
|
__local4__ = input.x
|
|
data.test.f(__local4__, __local2__)
|
|
__local1__ = __local2__
|
|
}
|
|
internal.template_string([__local5__], __local3__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, call, head set value",
|
|
module: `package test
|
|
f(x) := x
|
|
p contains $"{f(input.x)}"`,
|
|
exp: `package test
|
|
f(__local0__) := __local0__ if { true }
|
|
p contains __local3__ if {
|
|
__local5__ = {__local1__ |
|
|
__local4__ = input.x
|
|
data.test.f(__local4__, __local2__)
|
|
__local1__ = __local2__
|
|
}
|
|
internal.template_string([__local5__], __local3__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, call, head map key",
|
|
module: `package test
|
|
f(x) := x
|
|
p[$"{f(input.x)}"] := true`,
|
|
exp: `package test
|
|
f(__local1__) := __local1__ if { true }
|
|
p[__local0__] := true if {
|
|
__local6__ = {__local2__ |
|
|
__local5__ = input.x
|
|
data.test.f(__local5__, __local3__)
|
|
__local2__ = __local3__
|
|
}
|
|
internal.template_string([__local6__], __local4__)
|
|
__local0__ = __local4__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, call, in body",
|
|
module: `package test
|
|
f(x) := x
|
|
p := x if {
|
|
x := $"{f(input.x)}"
|
|
}`,
|
|
exp: `package test
|
|
f(__local0__) := __local0__ if { true }
|
|
p := __local1__ if {
|
|
__local6__ = {__local2__ |
|
|
__local5__ = input.x
|
|
data.test.f(__local5__, __local3__)
|
|
__local2__ = __local3__
|
|
}
|
|
internal.template_string([__local6__], __local4__)
|
|
__local1__ = __local4__
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("call infix template-expression", cases([]rewriteTest{{
|
|
note: "single template expression, infix, head value",
|
|
module: `package test
|
|
p := $"{input.x + 2}"`,
|
|
exp: `package test
|
|
p := __local2__ if {
|
|
__local4__ = {__local0__ |
|
|
__local3__ = input.x
|
|
plus(__local3__, 2, __local1__)
|
|
__local0__ = __local1__
|
|
}
|
|
internal.template_string([__local4__], __local2__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, infix, head set value",
|
|
module: `package test
|
|
p contains $"{input.x + 2}"`,
|
|
exp: `package test
|
|
p contains __local2__ if {
|
|
__local4__ = {__local0__ |
|
|
__local3__ = input.x
|
|
plus(__local3__, 2, __local1__)
|
|
__local0__ = __local1__
|
|
}
|
|
internal.template_string([__local4__], __local2__)
|
|
}`,
|
|
}, {
|
|
note: "single template expression, infix, head map key",
|
|
module: `package test
|
|
p[$"{input.x + 2}"] := true`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
__local5__ = {__local1__ |
|
|
__local4__ = input.x
|
|
plus(__local4__, 2, __local2__)
|
|
__local1__ = __local2__
|
|
}
|
|
internal.template_string([__local5__], __local3__)
|
|
__local0__ = __local3__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, infix, in body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"{input.x + 2}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
__local5__ = {__local1__ |
|
|
__local4__ = input.x
|
|
plus(__local4__, 2, __local2__)
|
|
__local1__ = __local2__
|
|
}
|
|
internal.template_string([__local5__], __local3__)
|
|
__local0__ = __local3__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, infix, equal (==), in body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"{input.x == 2}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
__local5__ = {__local1__ | __local4__ = input.x
|
|
equal(__local4__, 2, __local2__)
|
|
__local1__ = __local2__
|
|
}
|
|
internal.template_string([__local5__], __local3__)
|
|
__local0__ = __local3__
|
|
}`,
|
|
}, {
|
|
note: "single template expression, reference to default rule, not wrapped",
|
|
module: `package test
|
|
default x := 42
|
|
|
|
p := $"{x}"`,
|
|
exp: `package test
|
|
default x := 42
|
|
p := __local0__ if {
|
|
__local1__ = data.test.x
|
|
internal.template_string([{__local1__}], __local0__) }
|
|
`,
|
|
}, {
|
|
note: "single template expression, no exact reference to default rule, wrapped",
|
|
module: `package test
|
|
default a.b := {"c": 1}
|
|
a.b := {"d": 2} if input.x
|
|
|
|
p := $"{a.b.c}"`,
|
|
exp: `package test
|
|
default a.b := {"c": 1}
|
|
a.b := {"d": 2} if { input.x }
|
|
p := __local1__ if {
|
|
__local2__ = {__local0__ | __local0__ = data.test.a.b.c}
|
|
internal.template_string([__local2__], __local1__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("comprehensions", cases([]rewriteTest{{
|
|
note: "inside array comprehension, body",
|
|
module: `package test
|
|
p if {
|
|
[x | x := $"{input.x}"]
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
[__local0__ |
|
|
__local3__ = {__local1__ | __local1__ = input.x}
|
|
internal.template_string([__local3__], __local2__)
|
|
__local0__ = __local2__
|
|
]
|
|
}`,
|
|
}, {
|
|
note: "inside array comprehension, body, nested",
|
|
module: `package test
|
|
p if {
|
|
a := 1
|
|
[x |
|
|
b := 2
|
|
x := [y | y := $"{a} {b}"]
|
|
]
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local0__ = 1
|
|
[__local3__ |
|
|
__local1__ = 2
|
|
__local3__ = [__local2__ |
|
|
internal.template_string([{__local0__}, " ", {__local1__}], __local4__)
|
|
__local2__ = __local4__]
|
|
]
|
|
}`,
|
|
}, {
|
|
note: "inside array comprehension, head",
|
|
module: `package test
|
|
p if {
|
|
[$"{x} {input.y}" | x := input.x]
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
[__local2__ |
|
|
__local0__ = input.x
|
|
__local3__ = {__local1__ | __local1__ = input.y}
|
|
internal.template_string([{__local0__}, " ", __local3__], __local2__)
|
|
]
|
|
}`,
|
|
}, {
|
|
note: "inside array comprehension, head, nested",
|
|
module: `package test
|
|
p if {
|
|
a := 1
|
|
[x |
|
|
b := 2
|
|
x := [$"{a} {b} {c}" | c := 3 ]
|
|
]
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local0__ = 1
|
|
[__local3__ | __local1__ = 2
|
|
__local3__ = [__local4__ | __local2__ = 3
|
|
internal.template_string([{__local0__}, " ", {__local1__}, " ", {__local2__}], __local4__)
|
|
]
|
|
]
|
|
}`,
|
|
}, {
|
|
note: "inside set comprehension, body",
|
|
module: `package test
|
|
p if {
|
|
{x | x := $"{input.x}"}
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
{__local0__ |
|
|
__local3__ = {__local1__ | __local1__ = input.x}
|
|
internal.template_string([__local3__], __local2__)
|
|
__local0__ = __local2__
|
|
}
|
|
}`,
|
|
}, {
|
|
note: "inside set comprehension, head",
|
|
module: `package test
|
|
p if {
|
|
{$"{x} {input.y}" | x := input.x}
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
{__local2__ |
|
|
__local0__ = input.x
|
|
__local3__ = {__local1__ | __local1__ = input.y}
|
|
internal.template_string([{__local0__}, " ", __local3__], __local2__)}
|
|
}`,
|
|
}, {
|
|
note: "inside object comprehension, body",
|
|
module: `package test
|
|
p if {
|
|
{x: y |
|
|
x := $"{input.x}"
|
|
y := $"{input.y}"
|
|
}
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
{__local0__: __local1__ |
|
|
__local6__ = {__local2__ |
|
|
__local2__ = input.x
|
|
}
|
|
internal.template_string([__local6__], __local4__)
|
|
__local0__ = __local4__
|
|
__local7__ = {__local3__ |
|
|
__local3__ = input.y
|
|
}
|
|
internal.template_string([__local7__], __local5__)
|
|
__local1__ = __local5__
|
|
}
|
|
}`,
|
|
}, {
|
|
note: "inside object comprehension, head",
|
|
module: `package test
|
|
p if {
|
|
{$"{input.x} {y}": $"{x} {input.y}" |
|
|
x := input.x
|
|
y := input.y
|
|
}
|
|
}`,
|
|
exp: `package test
|
|
p = true if { {__local4__: __local5__ | __local0__ = input.x; __local1__ = input.y; __local6__ = {__local2__ | __local2__ = input.x}; internal.template_string([__local6__, " ", {__local1__}], __local4__); __local7__ = {__local3__ | __local3__ = input.y}; internal.template_string([{__local0__}, " ", __local7__], __local5__)} }`,
|
|
}, {
|
|
note: "single template expression, nested comprehension with local var, in function arg",
|
|
module: `package test
|
|
f($"{[x | y := input.ys[_]; x := y]}") := 42`,
|
|
exp: `package test
|
|
f(__local3__) := 42 if {
|
|
__local4__ = {__local2__ |
|
|
__local2__ = [__local1__ | __local0__ = input.ys[_]
|
|
__local1__ = __local0__]
|
|
}
|
|
internal.template_string([__local4__], __local3__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("every", cases([]rewriteTest{{
|
|
note: "inside every expression, body",
|
|
module: `package test
|
|
p if {
|
|
every i, x in input.l1 {
|
|
x == $"<{input.l2[i]}>"
|
|
}
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local3__ = input.l1
|
|
every __local0__, __local1__ in __local3__ {
|
|
__local5__ = {__local2__ |
|
|
__local2__ = input.l2[__local0__]
|
|
}
|
|
internal.template_string(["<", __local5__, ">"], __local4__)
|
|
__local1__ = __local4__
|
|
}
|
|
}`,
|
|
}, {
|
|
note: "inside every expression, domain",
|
|
module: `package test
|
|
p if {
|
|
every _, x in [$"{42} {input.x}"] {
|
|
x == $"42 foo"
|
|
}
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
__local5__ = {__local1__ | __local1__ = input.x}
|
|
internal.template_string([42, " ", __local5__], __local3__)
|
|
__local2__ = [__local3__]
|
|
every _, __local0__ in __local2__ {
|
|
internal.template_string(["42 foo"], __local4__)
|
|
__local0__ = __local4__
|
|
}
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("some", cases([]rewriteTest{{
|
|
note: "inside some",
|
|
module: `package test
|
|
users := {"alice_1", "alice_2"}
|
|
id := 1
|
|
|
|
t if {
|
|
$"user_{id}" in users
|
|
}`,
|
|
exp: `package test
|
|
users := {"alice_1", "alice_2"} if { true }
|
|
id := 1 if { true }
|
|
t = true if {
|
|
__local1__ = data.test.id
|
|
internal.template_string(["user_", {__local1__}], __local0__)
|
|
__local2__ = data.test.users
|
|
internal.member_2(__local0__, __local2__)
|
|
}`,
|
|
}, {
|
|
note: "inside some, domain",
|
|
module: `package test
|
|
t if {
|
|
some "user_1" in [$"alice_{1}", $"alice_{2}"]
|
|
}`,
|
|
exp: `package test
|
|
t = true if {
|
|
internal.template_string(["alice_", 1], __local2__)
|
|
internal.template_string(["alice_", 2], __local3__)
|
|
__local4__ = [__local2__, __local3__]
|
|
"user_1" = __local4__[__local1__]
|
|
}`,
|
|
}, {
|
|
note: "template string in head referencing var from some with template string in domain (issue #8162)",
|
|
module: `package test
|
|
r contains $"{val}" if {
|
|
some val in [1, $"{1 + 1}"]
|
|
}`,
|
|
exp: `package test
|
|
r contains __local4__ if {
|
|
__local8__ = {__local3__ | plus(1, 1, __local5__)
|
|
__local3__ = __local5__}
|
|
internal.template_string([__local8__], __local6__)
|
|
__local7__ = [1, __local6__];
|
|
__local2__ = __local7__[__local1__]
|
|
internal.template_string([{__local2__}], __local4__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("else", cases([]rewriteTest{{
|
|
note: "in else body",
|
|
module: `package test
|
|
p if {
|
|
false
|
|
} else := msg if {
|
|
msg := $"foo: {input.y}"
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
false
|
|
} else := __local0__ if {
|
|
__local3__ = {__local1__ |
|
|
__local1__ = input.y
|
|
}
|
|
internal.template_string(["foo: ", __local3__], __local2__)
|
|
__local0__ = __local2__
|
|
}`,
|
|
}, {
|
|
note: "in else head",
|
|
module: `package test
|
|
p if {
|
|
false
|
|
} else := $"foo: {input.y}"`,
|
|
exp: `package test
|
|
p = true if {
|
|
false
|
|
} else := __local1__ if {
|
|
__local2__ = {__local0__ |
|
|
__local0__ = input.y
|
|
}
|
|
internal.template_string(["foo: ", __local2__], __local1__)
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("nested template strings", cases([]rewriteTest{{
|
|
note: "body",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"foo {$"bar {data.a}"}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
__local6__ = {__local1__ |
|
|
__local5__ = {__local2__ |
|
|
__local2__ = data.a
|
|
}
|
|
internal.template_string(["bar ", __local5__], __local3__)
|
|
__local1__ = __local3__
|
|
}
|
|
internal.template_string(["foo ", __local6__], __local4__)
|
|
__local0__ = __local4__
|
|
}`,
|
|
}, {
|
|
note: "head value",
|
|
module: `package test
|
|
p := $"foo {$"bar {data.a}"}"`,
|
|
exp: `package test
|
|
p := __local3__ if {
|
|
__local5__ = {__local0__ |
|
|
__local4__ = {__local1__ |
|
|
__local1__ = data.a
|
|
}
|
|
internal.template_string(["bar ", __local4__], __local2__)
|
|
__local0__ = __local2__
|
|
}
|
|
internal.template_string(["foo ", __local5__], __local3__)
|
|
}`,
|
|
}, {
|
|
note: "head set value",
|
|
module: `package test
|
|
p contains $"foo {$"bar {data.a}"}"`,
|
|
exp: `package test
|
|
p contains __local3__ if {
|
|
__local5__ = {__local0__ |
|
|
__local4__ = {__local1__ |
|
|
__local1__ = data.a
|
|
}
|
|
internal.template_string(["bar ", __local4__], __local2__)
|
|
__local0__ = __local2__
|
|
}
|
|
internal.template_string(["foo ", __local5__], __local3__)
|
|
}`,
|
|
}, {
|
|
note: "head map key",
|
|
module: `package test
|
|
p[$"foo {$"bar {data.a}"}"] := true`,
|
|
exp: `package test
|
|
p[__local0__] := true if {
|
|
__local6__ = {__local1__ |
|
|
__local5__ = {__local2__ |
|
|
__local2__ = data.a
|
|
}
|
|
internal.template_string(["bar ", __local5__], __local3__)
|
|
__local1__ = __local3__
|
|
}
|
|
internal.template_string(["foo ", __local6__], __local4__)
|
|
__local0__ = __local4__
|
|
}`,
|
|
}, {
|
|
note: "inner template in head of comprehension",
|
|
module: `package test
|
|
p := x if {
|
|
x := $"foo {[$"bar {x} {input.y}" | x := input.x]}"
|
|
}`,
|
|
exp: `package test
|
|
p := __local1__ if {
|
|
__local7__ = {__local2__ |
|
|
__local2__ = [__local4__ |
|
|
__local0__ = input.x
|
|
__local6__ = {__local3__ |
|
|
__local3__ = input.y
|
|
}
|
|
internal.template_string(["bar ", {__local0__}, " ", __local6__], __local4__)
|
|
]
|
|
}
|
|
internal.template_string(["foo ", __local7__], __local5__)
|
|
__local1__ = __local5__
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("with", cases([]rewriteTest{{
|
|
note: "modifier inside template-expression",
|
|
module: `package test
|
|
a := input
|
|
p := $"{a with input as 42} {a with input as {"x": true}}"`,
|
|
exp: `package test
|
|
a := __local3__ if { __local3__ = input }
|
|
p := __local2__ if {
|
|
__local4__ = {__local0__ | __local0__ = data.test.a with input as 42}
|
|
__local5__ = {__local1__ | __local1__ = data.test.a with input as {"x": true}}
|
|
internal.template_string([__local4__, " ", __local5__], __local2__)
|
|
}`,
|
|
}, {
|
|
note: "modifier outside string-template",
|
|
module: `package test
|
|
a := input
|
|
b := input
|
|
p if {
|
|
$"{a} {b}" with input as 42
|
|
}`,
|
|
exp: `package test
|
|
a := __local3__ if { __local3__ = input }
|
|
b := __local4__ if { __local4__ = input }
|
|
p = true if {
|
|
__local5__ = {__local0__ | __local0__ = data.test.a} with input as 42
|
|
__local6__ = {__local1__ | __local1__ = data.test.b} with input as 42
|
|
internal.template_string([__local5__, " ", __local6__], __local2__) with input as 42
|
|
__local2__ with input as 42
|
|
}`,
|
|
}, {
|
|
note: "modifier inside template-expression and outside string-template",
|
|
module: `package test
|
|
a := input.x + input.y
|
|
p := x if {
|
|
x := $"{a with input.x as 1}" with input.y as 2
|
|
}`,
|
|
exp: `package test
|
|
a := __local2__ if {
|
|
__local4__ = input.x
|
|
__local5__ = input.y
|
|
plus(__local4__, __local5__, __local2__)
|
|
}
|
|
p := __local0__ if {
|
|
__local6__ = {__local1__ | __local1__ = data.test.a with input.x as 1} with input.y as 2
|
|
internal.template_string([__local6__], __local3__) with input.y as 2
|
|
__local0__ = __local3__ with input.y as 2
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("other", cases([]rewriteTest{{
|
|
note: "var used in template-expression preceding assignment through unification",
|
|
module: `package test
|
|
p := msg if {
|
|
msg := $"{x}"
|
|
x = 42
|
|
}`,
|
|
exp: `package test
|
|
p := __local0__ if {
|
|
__local0__ = __local1__
|
|
x = 42;
|
|
internal.template_string([{x}], __local1__)
|
|
}`,
|
|
}, {
|
|
note: "refs to known defined rules are not wrapped in comprehensions",
|
|
module: `package test
|
|
default a.b := "c"
|
|
pi := 3.14
|
|
multi contains "value"
|
|
|
|
result := $"{a.b} {pi} {multi}"`,
|
|
exp: `package test
|
|
default a.b := "c"
|
|
pi := 3.14 if { true }
|
|
multi contains "value" if { true }
|
|
|
|
result := __local0__ if {
|
|
__local1__ = data.test.a.b
|
|
__local2__ = data.test.pi
|
|
__local3__ = data.test.multi
|
|
internal.template_string([{__local1__}, " ", {__local2__}, " ", {__local3__}], __local0__)
|
|
}`,
|
|
}, {
|
|
note: "attribute ref of safe var is still not known to be safe, and gets wrapped",
|
|
module: `package test
|
|
p := msg if {
|
|
x := object.union({"a": 1}, {"b": 2})
|
|
msg := $"{x.c}"
|
|
}`,
|
|
exp: `package test
|
|
|
|
p := __local1__ if {
|
|
object.union({"a": 1}, {"b": 2}, __local3__)
|
|
__local0__ = __local3__
|
|
__local5__ = {__local2__ | __local2__ = __local0__.c}
|
|
internal.template_string([__local5__], __local4__)
|
|
__local1__ = __local4__
|
|
}
|
|
`,
|
|
}}))
|
|
|
|
t.Run("not", cases([]rewriteTest{{
|
|
note: "single template expression, in implicit not-body",
|
|
module: `package test
|
|
import future.keywords.not
|
|
p if {
|
|
not $"{input.x}"
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
not {
|
|
__local2__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local2__], __local1__)
|
|
__local1__
|
|
}
|
|
}`,
|
|
}, {
|
|
note: "single template expression, in explicit not-body",
|
|
module: `package test
|
|
import future.keywords.not
|
|
p if {
|
|
not { $"{input.x}" }
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
not {
|
|
__local2__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local2__], __local1__)
|
|
__local1__
|
|
}
|
|
}`,
|
|
}}))
|
|
|
|
t.Run("logical operators", cases([]rewriteTest{{
|
|
note: "or",
|
|
module: `package test
|
|
import future.keywords.or
|
|
p if {
|
|
$"{input.x}" or { $"{input.y}" }
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
{
|
|
__local4__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local4__], __local2__)
|
|
__local2__
|
|
} or {
|
|
__local5__ = {__local1__ | __local1__ = input.y}
|
|
internal.template_string([__local5__], __local3__)
|
|
__local3__
|
|
}
|
|
}`,
|
|
}, {
|
|
note: "and",
|
|
module: `package test
|
|
import future.keywords.and
|
|
p if {
|
|
$"{input.x}" and { $"{input.y}" }
|
|
}`,
|
|
exp: `package test
|
|
p = true if {
|
|
{
|
|
__local4__ = {__local0__ | __local0__ = input.x}
|
|
internal.template_string([__local4__], __local2__)
|
|
__local2__
|
|
} and {
|
|
__local5__ = {__local1__ | __local1__ = input.y}
|
|
internal.template_string([__local5__], __local3__)
|
|
__local3__
|
|
}
|
|
}`,
|
|
}}))
|
|
}
|
|
|
|
func TestCompilerRewriteTemplateStringsErrors(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "undeclared var, rule head",
|
|
module: `package test
|
|
p := $"{x}"`,
|
|
exp: "var x is unsafe",
|
|
},
|
|
{
|
|
note: "undeclared var, rule body",
|
|
module: `package test
|
|
p := msg if {
|
|
msg := $"{x}"
|
|
}`,
|
|
exp: "var x is unsafe",
|
|
},
|
|
{
|
|
note: "undeclared var (wildcard)",
|
|
module: `package test
|
|
p := msg if {
|
|
a := ["a", "b"]
|
|
msg := $"{a[_]}"
|
|
}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var (enum)",
|
|
module: `package test
|
|
p := msg if {
|
|
a := ["a", "b"]
|
|
msg := $"{a[x]}"
|
|
}`,
|
|
exp: "var x is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, nested inside template-string",
|
|
module: `package test
|
|
p := $"{$"{x}"}"`,
|
|
exp: "var x is unsafe",
|
|
},
|
|
{
|
|
note: "undeclared var, inside array comprehension body",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := [x | x := $"{a[_]}"]`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside array comprehension head",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := [$"{a[_]}" | x := 42]`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside set comprehension body",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := {x | x := $"{a[_]}"}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside set comprehension head",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := {$"{a[_]}" | x := 42}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside object comprehension body",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := {x: y | x := $"{a[_]}"; y := 42}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside object comprehension key",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := {$"{a[_]}": 42 | x := 42}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside object comprehension value",
|
|
module: `package test
|
|
a := ["a", "b"]
|
|
p := {42: $"{a[_]}" | x := 42}`,
|
|
exp: "var _ is undeclared",
|
|
},
|
|
{
|
|
note: "undeclared var, inside every domain",
|
|
module: `package test
|
|
p if {
|
|
every x in {"a", $"{x}"} {
|
|
x != "b"
|
|
}
|
|
}`,
|
|
exp: "var x is unsafe",
|
|
},
|
|
{
|
|
note: "undeclared var, inside every body",
|
|
module: `package test
|
|
p if {
|
|
every x in {"a", "b"} {
|
|
x != $"{y}"
|
|
}
|
|
}`,
|
|
exp: "var y is unsafe",
|
|
},
|
|
{
|
|
note: "walk built-in call",
|
|
module: `package test
|
|
p := $"{walk(["a", "b"])}"`,
|
|
exp: "illegal call to relation built-in 'walk' that may cause multiple outputs",
|
|
},
|
|
{
|
|
note: "undeclared var, some-in with undeclared collection (issue #8157)",
|
|
module: `package test
|
|
items contains item if {
|
|
some label in labels
|
|
item := $"{label}"
|
|
}`,
|
|
exp: "contains: is unsafe",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithEnablePrintStatements(true)
|
|
c.Compile(map[string]*Module{
|
|
"test.rego": module(tc.module),
|
|
})
|
|
if !c.Failed() {
|
|
t.Fatal("expected error, got none")
|
|
}
|
|
if c.Errors[0].Message != tc.exp {
|
|
if after, ok := strings.CutPrefix(tc.exp, "contains:"); ok {
|
|
if exp := after; !strings.Contains(c.Errors[0].Message, exp) {
|
|
t.Fatalf("expected error containing:\n\n%s\n\ngot:\n\n%s", tc.exp, c.Errors[0].Message)
|
|
}
|
|
} else {
|
|
t.Fatalf("expected error:\n\n%s\n\ngot:\n\n%s", tc.exp, c.Errors[0].Message)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRewritePrintCallsWithElseImplicitArgs(t *testing.T) {
|
|
|
|
mod := `package test
|
|
|
|
f(x, y) if {
|
|
x = y
|
|
}
|
|
|
|
else = false if {
|
|
print(x, y)
|
|
}`
|
|
|
|
c := NewCompiler().WithEnablePrintStatements(true)
|
|
c.Compile(map[string]*Module{
|
|
"test.rego": module(mod),
|
|
})
|
|
|
|
if c.Failed() {
|
|
t.Fatal(c.Errors)
|
|
}
|
|
|
|
exp := module(`package test
|
|
|
|
f(__local0__, __local1__) = true if { __local0__ = __local1__ }
|
|
else = false if { __local4__ = {__local2__ | __local2__ = __local0__}; __local5__ = {__local3__ | __local3__ = __local1__}; internal.print([__local4__, __local5__]) }
|
|
`)
|
|
|
|
// NOTE(tsandall): we have to patch the implicit args on the else rule
|
|
// because of how the parser copies the arg names across from the first
|
|
// rule.
|
|
exp.Rules[0].Else.Head.Args[0] = VarTerm("__local0__")
|
|
exp.Rules[0].Else.Head.Args[1] = VarTerm("__local1__")
|
|
|
|
if !exp.Equal(c.Modules["test.rego"]) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp, c.Modules["test.rego"])
|
|
}
|
|
}
|
|
|
|
func TestCompilerMockFunction(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module, extra string
|
|
err string
|
|
}{
|
|
{
|
|
note: "simple valid",
|
|
module: `package test
|
|
now() = 123
|
|
p if { true with time.now_ns as now }
|
|
`,
|
|
},
|
|
{
|
|
note: "simple valid, simple name",
|
|
module: `package test
|
|
mock_concat(_, _) = "foo/bar"
|
|
p if { concat("/", input) with concat as mock_concat }
|
|
`,
|
|
},
|
|
{
|
|
note: "invalid ref: nonexistant",
|
|
module: `package test
|
|
p if { true with time.now_ns as now }
|
|
`,
|
|
err: "rego_unsafe_var_error: var now is unsafe", // we're running all compiler stages here
|
|
},
|
|
{
|
|
note: "valid ref: not a function, but arity = 0",
|
|
module: `package test
|
|
now = 1
|
|
p if { true with time.now_ns as now }
|
|
`,
|
|
},
|
|
{
|
|
note: "ref: not a function, arity > 0",
|
|
module: `package test
|
|
http_send = { "body": "nope" }
|
|
p if { true with http.send as http_send }
|
|
`,
|
|
},
|
|
{
|
|
note: "invalid ref: arity mismatch",
|
|
module: `package test
|
|
http_send(_, _) = { "body": "nope" }
|
|
p if { true with http.send as http_send }
|
|
`,
|
|
err: "rego_type_error: http.send: arity mismatch\n\thave: (any, any)\n\twant: (request: object[string: any])",
|
|
},
|
|
{
|
|
note: "invalid ref: arity mismatch (in call)",
|
|
module: `package test
|
|
http_send(_, _) = { "body": "nope" }
|
|
p if { http.send({}) with http.send as http_send }
|
|
`,
|
|
err: "rego_type_error: http.send: arity mismatch\n\thave: (any, any)\n\twant: (request: object[string: any])",
|
|
},
|
|
{
|
|
note: "invalid ref: value another built-in with different type",
|
|
module: `package test
|
|
p if { true with http.send as net.lookup_ip_addr }
|
|
`,
|
|
err: "rego_type_error: http.send: arity mismatch\n\thave: (string)\n\twant: (request: object[string: any])",
|
|
},
|
|
{
|
|
note: "ref: value another built-in with compatible type",
|
|
module: `package test
|
|
p if { true with count as object.union_n }
|
|
`,
|
|
},
|
|
{
|
|
note: "valid: package import",
|
|
extra: `package mocks
|
|
http_send(_) = {}
|
|
`,
|
|
module: `package test
|
|
import data.mocks
|
|
p if { true with http.send as mocks.http_send }
|
|
`,
|
|
},
|
|
{
|
|
note: "valid: function import",
|
|
extra: `package mocks
|
|
http_send(_) = {}
|
|
`,
|
|
module: `package test
|
|
import data.mocks.http_send
|
|
p if { true with http.send as http_send }
|
|
`,
|
|
},
|
|
{
|
|
note: "invalid target: relation",
|
|
module: `package test
|
|
my_walk(_, _)
|
|
p if { true with walk as my_walk }
|
|
`,
|
|
err: "rego_compile_error: with keyword replacing built-in function: target must not be a relation",
|
|
},
|
|
{
|
|
note: "invalid target: eq",
|
|
module: `package test
|
|
my_eq(_, _)
|
|
p if { true with eq as my_eq }
|
|
`,
|
|
err: `rego_compile_error: with keyword replacing built-in function: replacement of "eq" invalid`,
|
|
},
|
|
{
|
|
note: "invalid target: rego.metadata.chain",
|
|
module: `package test
|
|
p if { true with rego.metadata.chain as [] }
|
|
`,
|
|
err: `rego_compile_error: with keyword replacing built-in function: replacement of "rego.metadata.chain" invalid`,
|
|
},
|
|
{
|
|
note: "invalid target: rego.metadata.rule",
|
|
module: `package test
|
|
p if { true with rego.metadata.rule as {} }
|
|
`,
|
|
err: `rego_compile_error: with keyword replacing built-in function: replacement of "rego.metadata.rule" invalid`,
|
|
},
|
|
{
|
|
note: "invalid target: internal.print",
|
|
module: `package test
|
|
my_print(_, _)
|
|
p if { true with internal.print as my_print }
|
|
`,
|
|
err: `rego_compile_error: with keyword replacing built-in function: replacement of internal function "internal.print" invalid`,
|
|
},
|
|
{
|
|
note: "mocking custom built-in",
|
|
module: `package test
|
|
mock(_)
|
|
mock_mock(_)
|
|
p if { bar(foo.bar("one")) with bar as mock with foo.bar as mock_mock }
|
|
`,
|
|
},
|
|
{
|
|
note: "non-built-in function replaced value",
|
|
module: `package test
|
|
original(_)
|
|
p if { original(true) with original as 123 }
|
|
`,
|
|
},
|
|
{
|
|
note: "non-built-in function replaced by another, arity 0",
|
|
module: `package test
|
|
original() = 1
|
|
mock() = 2
|
|
p if { original() with original as mock }
|
|
`,
|
|
err: "rego_type_error: undefined function data.test.original", // TODO(sr): file bug -- this doesn't depend on "with" used or not
|
|
},
|
|
{
|
|
note: "non-built-in function replaced by another, arity 1",
|
|
module: `package test
|
|
original(_)
|
|
mock(_)
|
|
p if { original(true) with original as mock }
|
|
`,
|
|
},
|
|
{
|
|
note: "non-built-in function replaced by built-in",
|
|
module: `package test
|
|
original(_)
|
|
p if { original([1]) with original as count }
|
|
`,
|
|
},
|
|
{
|
|
note: "non-built-in function replaced by another, arity mismatch",
|
|
module: `package test
|
|
original(_)
|
|
mock(_, _)
|
|
p if { original([1]) with original as mock }
|
|
`,
|
|
err: "rego_type_error: data.test.original: arity mismatch\n\thave: (any, any)\n\twant: (any)",
|
|
},
|
|
{
|
|
note: "non-built-in function replaced by built-in, arity mismatch",
|
|
module: `package test
|
|
original(_)
|
|
p if { original([1]) with original as concat }
|
|
`,
|
|
err: "rego_type_error: data.test.original: arity mismatch\n\thave: (string, any<array[string], set[string]>)\n\twant: (any)",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler().WithBuiltins(map[string]*Builtin{
|
|
"bar": {
|
|
Name: "bar",
|
|
Decl: types.NewFunction([]types.Type{types.S}, types.A),
|
|
},
|
|
"foo.bar": {
|
|
Name: "foo.bar",
|
|
Decl: types.NewFunction([]types.Type{types.S}, types.A),
|
|
},
|
|
})
|
|
if tc.extra != "" {
|
|
c.Modules["extra"] = module(tc.extra)
|
|
}
|
|
c.Modules["test"] = module(tc.module)
|
|
|
|
// NOTE(sr): We're running all compiler stages here, since the type checking of
|
|
// built-in function replacements happens at the type check stage.
|
|
c.Compile(c.Modules)
|
|
|
|
if tc.err != "" {
|
|
if !strings.Contains(c.Errors.Error(), tc.err) {
|
|
t.Errorf("expected error to contain %q, got %q", tc.err, c.Errors.Error())
|
|
}
|
|
} else if len(c.Errors) > 0 {
|
|
t.Errorf("expected no errors, got %v", c.Errors)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerMockVirtualDocumentPartially(t *testing.T) {
|
|
c := NewCompiler()
|
|
|
|
c.Modules["test"] = module(`
|
|
package test
|
|
p = {"a": 1}
|
|
q = x if { p = x with p.a as 2 }
|
|
`)
|
|
|
|
compileStages(c, StageRewriteWithValues)
|
|
assertCompilerErrorStrings(t, c, []string{"rego_compile_error: with keyword cannot partially replace virtual document(s)"})
|
|
}
|
|
|
|
func TestCompilerCheckUnusedAssignedVar(t *testing.T) {
|
|
type testCase struct {
|
|
note string
|
|
module string
|
|
expectedErrors Errors
|
|
}
|
|
|
|
cases := []testCase{
|
|
{
|
|
note: "global var",
|
|
module: `package test
|
|
x := 1
|
|
`,
|
|
},
|
|
{
|
|
note: "simple rule with wildcard",
|
|
module: `package test
|
|
p if {
|
|
_ := 1
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "simple rule",
|
|
module: `package test
|
|
p if {
|
|
x := 1
|
|
y := 2
|
|
z := x + 3
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with return",
|
|
module: `package test
|
|
p = x if {
|
|
x := 2
|
|
y := 3
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with function call",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := f(x)
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested array comprehension",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := [z | z := 2 * x]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested array comprehension and shadowing",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := [x | x := 2 * x]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested array comprehension and shadowing (unused shadowed var)",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := [x | x := 2]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var x unused"},
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested array comprehension and shadowing (unused shadowing var)",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
x > 1
|
|
[1 | x := 2]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var x unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested array comprehension and some declaration",
|
|
module: `package test
|
|
p if {
|
|
some i
|
|
_ := [z | z := [1, 2][i]]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "rule with nested set comprehension",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := {z | z := 2 * x}
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested set comprehension and unused inner var",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := {z | z := 2 * x; a := 2}
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var a unused"}, // y isn't reported, as we abort early on errors when moving through the stack
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested object comprehension",
|
|
module: `package test
|
|
p if {
|
|
x := 2
|
|
y := {z: x | z := 2 * x}
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested closure",
|
|
module: `package test
|
|
p if {
|
|
x := 1
|
|
a := 1
|
|
{ y | y := [ z | z:=[1,2,3][a]; z > 1 ][_] }
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var x unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "rule with nested closure and unused inner var",
|
|
module: `package test
|
|
p if {
|
|
x := 1
|
|
{ y | y := [ z | z:=[1,2,3][x]; z > 1; a := 2 ][_] }
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var a unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "simple function",
|
|
module: `package test
|
|
f() if {
|
|
x := 1
|
|
y := 2
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var x unused"},
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "simple function with wildcard",
|
|
module: `package test
|
|
f() if {
|
|
x := 1
|
|
_ := 2
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var x unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "function with return",
|
|
module: `package test
|
|
f() = x if {
|
|
x := 1
|
|
y := 2
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension",
|
|
module: `package test
|
|
comp = [ 1 |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension nested",
|
|
module: `package test
|
|
comp := [ 1 |
|
|
x := 1
|
|
y := [a | a := x]
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension with wildcard",
|
|
module: `package test
|
|
comp = [ 1 |
|
|
x := [1, 2, 3]
|
|
_ := 2
|
|
z := x[_]
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension with return",
|
|
module: `package test
|
|
comp = [ z |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "array comprehension with some",
|
|
module: `package test
|
|
comp = [ i |
|
|
some i
|
|
y := 2
|
|
]
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension",
|
|
module: `package test
|
|
comp = { 1 |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension nested",
|
|
module: `package test
|
|
comp := { 1 |
|
|
x := 1
|
|
y := [a | a := x]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension with wildcard",
|
|
module: `package test
|
|
comp = { 1 |
|
|
x := [1, 2, 3]
|
|
_ := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension with return",
|
|
module: `package test
|
|
comp = { z |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "set comprehension with some",
|
|
module: `package test
|
|
comp = { i |
|
|
some i
|
|
y := 2
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension",
|
|
module: `package test
|
|
comp = { 1: 2 |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension nested",
|
|
module: `package test
|
|
comp := { 1: 1 |
|
|
x := 1
|
|
y := {a: x | a := x}
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension with wildcard",
|
|
module: `package test
|
|
comp = { 1: 2 |
|
|
x := [1, 2, 3]
|
|
_ := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var z unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension with return",
|
|
module: `package test
|
|
comp = { z: x |
|
|
x := [1, 2, 3]
|
|
y := 2
|
|
z := x[_]
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "object comprehension with some",
|
|
module: `package test
|
|
comp = { i |
|
|
some i
|
|
y := 2
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "every: unused assigned var in body",
|
|
module: `package test
|
|
p if { every i in [1] { y := 10; i == 1 } }
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var y unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "general ref in rule head",
|
|
module: `package test
|
|
p[q].r[s] := 1 if {
|
|
q := "foo"
|
|
s := "bar"
|
|
t := "baz"
|
|
}
|
|
`,
|
|
expectedErrors: Errors{
|
|
&Error{Message: "assigned var t unused"},
|
|
},
|
|
},
|
|
{
|
|
note: "general ref in rule head (no errors)",
|
|
module: `package test
|
|
p[q].r[s] := 1 if {
|
|
q := "foo"
|
|
s := "bar"
|
|
}
|
|
`,
|
|
expectedErrors: Errors{},
|
|
},
|
|
}
|
|
|
|
makeTestRunner := func(tc testCase, strict bool) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
compiler := NewCompiler().WithStrict(strict)
|
|
compiler.Modules = map[string]*Module{
|
|
"test": module(tc.module),
|
|
}
|
|
compileStages(compiler, StageRewriteLocalVars)
|
|
|
|
if strict {
|
|
assertErrors(t, compiler.Errors, tc.expectedErrors, false)
|
|
} else {
|
|
assertNotFailed(t, compiler)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note+"_strict", makeTestRunner(tc, true))
|
|
t.Run(tc.note+"_non-strict", makeTestRunner(tc, false))
|
|
}
|
|
}
|
|
|
|
func TestCompilerSetGraph(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = getCompilerTestModules()
|
|
c.Modules["elsekw"] = module(`
|
|
package elsekw
|
|
|
|
p if {
|
|
false
|
|
} else = q if {
|
|
false
|
|
} else if {
|
|
r
|
|
}
|
|
|
|
q = true
|
|
r = true
|
|
|
|
s if { t }
|
|
t if { false } else if { true }
|
|
|
|
`)
|
|
compileStages(c, StageSetGraph)
|
|
|
|
assertNotFailed(t, c)
|
|
|
|
mod1 := c.Modules["mod1"]
|
|
p := mod1.Rules[0]
|
|
q := mod1.Rules[1]
|
|
mod2 := c.Modules["mod2"]
|
|
r := mod2.Rules[0]
|
|
mod5 := c.Modules["mod5"]
|
|
|
|
edges := map[util.T]struct{}{
|
|
q: {},
|
|
r: {},
|
|
}
|
|
|
|
if !reflect.DeepEqual(edges, c.Graph.Dependencies(p)) {
|
|
t.Fatalf("Expected dependencies for p to be q and r but got: %v", c.Graph.Dependencies(p))
|
|
}
|
|
|
|
// NOTE(tsandall): this is the correct result but it's chosen arbitrarily for the test.
|
|
expDependents := []struct {
|
|
x *Rule
|
|
want map[util.T]struct{}
|
|
}{
|
|
{
|
|
x: p,
|
|
want: nil,
|
|
},
|
|
{
|
|
x: q,
|
|
want: map[util.T]struct{}{p: {}, mod5.Rules[1]: {}, mod5.Rules[3]: {}, mod5.Rules[5]: {}},
|
|
},
|
|
{
|
|
x: r,
|
|
want: map[util.T]struct{}{p: {}},
|
|
},
|
|
}
|
|
|
|
for _, exp := range expDependents {
|
|
if !reflect.DeepEqual(exp.want, c.Graph.Dependents(exp.x)) {
|
|
t.Fatalf("Expected dependents for %v to be %v but got: %v", exp.x, exp.want, c.Graph.Dependents(exp.x))
|
|
}
|
|
}
|
|
|
|
sorted, ok := c.Graph.Sort()
|
|
if !ok {
|
|
t.Fatalf("Expected sort to succeed.")
|
|
}
|
|
|
|
numRules := 0
|
|
|
|
for _, module := range c.Modules {
|
|
WalkRules(module, func(*Rule) bool {
|
|
numRules++
|
|
return false
|
|
})
|
|
}
|
|
|
|
if len(sorted) != numRules {
|
|
t.Fatalf("Expected numRules (%v) to be same as len(sorted) (%v)", numRules, len(sorted))
|
|
}
|
|
|
|
// Probe rules with dependencies. Ordering is not stable for ties because
|
|
// nodes are stored in a map.
|
|
probes := [][2]*Rule{
|
|
{c.Modules["mod1"].Rules[1], c.Modules["mod1"].Rules[0]}, // mod1.q before mod1.p
|
|
{c.Modules["mod2"].Rules[0], c.Modules["mod1"].Rules[0]}, // mod2.r before mod1.p
|
|
{c.Modules["mod1"].Rules[1], c.Modules["mod5"].Rules[1]}, // mod1.q before mod5.r
|
|
{c.Modules["mod1"].Rules[1], c.Modules["mod5"].Rules[3]}, // mod1.q before mod6.t
|
|
{c.Modules["mod1"].Rules[1], c.Modules["mod5"].Rules[5]}, // mod1.q before mod6.v
|
|
{c.Modules["mod6"].Rules[2], c.Modules["mod6"].Rules[3]}, // mod6.r before mod6.s
|
|
{c.Modules["elsekw"].Rules[1], c.Modules["elsekw"].Rules[0].Else}, // elsekw.q before elsekw.p.else
|
|
{c.Modules["elsekw"].Rules[2], c.Modules["elsekw"].Rules[0].Else.Else}, // elsekw.r before elsekw.p.else.else
|
|
{c.Modules["elsekw"].Rules[4], c.Modules["elsekw"].Rules[3]}, // elsekw.t before elsekw.s
|
|
{c.Modules["elsekw"].Rules[4].Else, c.Modules["elsekw"].Rules[3]}, // elsekw.t.else before elsekw.s
|
|
}
|
|
|
|
getSortedIdx := func(r *Rule) int {
|
|
for i := range sorted {
|
|
if sorted[i] == r {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
for num, probe := range probes {
|
|
i := getSortedIdx(probe[0])
|
|
j := getSortedIdx(probe[1])
|
|
if i == -1 || j == -1 {
|
|
t.Fatalf("Expected to find probe %d in sorted slice but got: i=%d, j=%d", num+1, i, j)
|
|
}
|
|
if i >= j {
|
|
t.Errorf("Sort order of probe %d (A) %v and (B) %v and is wrong (expected A before B)", num+1, probe[0], probe[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGraphCycle(t *testing.T) {
|
|
mod1 := `package a.b.c
|
|
|
|
p if { q }
|
|
q if { r }
|
|
r if { s }
|
|
s if { q }`
|
|
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"mod1": module(mod1),
|
|
}
|
|
|
|
compileStages(c, StageSetGraph)
|
|
assertNotFailed(t, c)
|
|
|
|
_, ok := c.Graph.Sort()
|
|
if ok {
|
|
t.Fatalf("Expected to find cycle in rule graph")
|
|
}
|
|
|
|
elsekw := `package elsekw
|
|
|
|
p if {
|
|
false
|
|
} else = q if {
|
|
true
|
|
}
|
|
|
|
q if {
|
|
false
|
|
} else if {
|
|
r
|
|
}
|
|
|
|
r if { s }
|
|
|
|
s if { p }
|
|
`
|
|
|
|
c = NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"elsekw": module(elsekw),
|
|
}
|
|
|
|
compileStages(c, StageSetGraph)
|
|
assertNotFailed(t, c)
|
|
|
|
_, ok = c.Graph.Sort()
|
|
if ok {
|
|
t.Fatalf("Expected to find cycle in rule graph")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerCheckRecursion(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{
|
|
"newMod1": module(`package rec
|
|
|
|
s = true if { t }
|
|
t = true if { s }
|
|
a = true if { b }
|
|
b = true if { c }
|
|
c = true if { d; e }
|
|
d = true if { true }
|
|
e = true if { a }`),
|
|
"newMod2": module(`package rec
|
|
|
|
x = true if { s }`,
|
|
),
|
|
"newMod3": module(`package rec2
|
|
|
|
import data.rec.x
|
|
|
|
y = true if { x }`),
|
|
"newMod4": module(`package rec3
|
|
|
|
p[x] = y if { data.rec4[x][y] = z }`,
|
|
),
|
|
"newMod5": module(`package rec4
|
|
|
|
import data.rec3.p
|
|
|
|
q[x] = y if { p[x] = y }`),
|
|
"newMod6": module(`package rec5
|
|
|
|
acp contains x if { acq[x] }
|
|
acq contains x if { a = [true | acp[_]]; a[_] = x }
|
|
`,
|
|
),
|
|
"newMod7": module(`package rec6
|
|
|
|
np[x] = y if { data.a[data.b.c[nq[x]]] = y }
|
|
nq[x] = y if { data.d[data.e[x].f[np[y]]] }`,
|
|
),
|
|
"newMod8": module(`package rec7
|
|
|
|
prefix = true if { data.rec7 }`,
|
|
),
|
|
"newMod9": module(`package rec8
|
|
|
|
dataref = true if { data }`,
|
|
),
|
|
"newMod10": module(`package rec9
|
|
|
|
else_self if { false } else if { else_self }
|
|
|
|
elsetop if {
|
|
false
|
|
} else = elsemid if {
|
|
true
|
|
}
|
|
|
|
elsemid if {
|
|
false
|
|
} else if {
|
|
elsebottom
|
|
}
|
|
|
|
elsebottom if { elsetop }
|
|
`),
|
|
"fnMod1": module(`package f0
|
|
|
|
fn(x) = y if {
|
|
fn(x, y)
|
|
}`),
|
|
"fnMod2": module(`package f1
|
|
|
|
foo(x) = y if {
|
|
bar("buz", x, y)
|
|
}
|
|
|
|
bar(x, y) = z if {
|
|
foo([x, y], z)
|
|
}`),
|
|
"fnMod3": module(`package f2
|
|
|
|
foo(x) = y if {
|
|
bar("buz", x, y)
|
|
}
|
|
|
|
bar(x, y) = z if {
|
|
x = p[y]
|
|
z = x
|
|
}
|
|
|
|
p[x] = y if {
|
|
x = "foo.bar"
|
|
foo(x, y)
|
|
}`),
|
|
"everyMod": module(`package everymod
|
|
import future.keywords.every
|
|
everyp if {
|
|
every x in [true, false] { x; everyp }
|
|
}
|
|
everyq contains 1 if {
|
|
every x in everyq { x == 1 }
|
|
}`),
|
|
}
|
|
|
|
compileStages(c, StageCheckRecursion)
|
|
|
|
makeRuleErrMsg := func(pkg, rule string, loop ...string) string {
|
|
l := make([]string, len(loop))
|
|
for i, lo := range loop {
|
|
l[i] = "data." + pkg + "." + lo
|
|
}
|
|
return fmt.Sprintf("rego_recursion_error: rule data.%s.%s is recursive: %v", pkg, rule, strings.Join(l, " -> "))
|
|
}
|
|
|
|
expected := []string{
|
|
makeRuleErrMsg("rec", "s", "s", "t", "s"),
|
|
makeRuleErrMsg("rec", "t", "t", "s", "t"),
|
|
makeRuleErrMsg("rec", "a", "a", "b", "c", "e", "a"),
|
|
makeRuleErrMsg("rec", "b", "b", "c", "e", "a", "b"),
|
|
makeRuleErrMsg("rec", "c", "c", "e", "a", "b", "c"),
|
|
makeRuleErrMsg("rec", "e", "e", "a", "b", "c", "e"),
|
|
`rego_recursion_error: rule data.rec3.p[x] is recursive: data.rec3.p[x] -> data.rec4.q[x] -> data.rec3.p[x]`, // NOTE(sr): these two are hardcoded: they are
|
|
`rego_recursion_error: rule data.rec4.q[x] is recursive: data.rec4.q[x] -> data.rec3.p[x] -> data.rec4.q[x]`, // the only ones not fitting the pattern.
|
|
makeRuleErrMsg("rec5", "acq", "acq", "acp", "acq"),
|
|
makeRuleErrMsg("rec5", "acp", "acp", "acq", "acp"),
|
|
makeRuleErrMsg("rec6", "np[x]", "np[x]", "nq[x]", "np[x]"),
|
|
makeRuleErrMsg("rec6", "nq[x]", "nq[x]", "np[x]", "nq[x]"),
|
|
makeRuleErrMsg("rec7", "prefix", "prefix", "prefix"),
|
|
makeRuleErrMsg("rec8", "dataref", "dataref", "dataref"),
|
|
makeRuleErrMsg("rec9", "else_self", "else_self", "else_self"),
|
|
makeRuleErrMsg("rec9", "elsetop", "elsetop", "elsemid", "elsebottom", "elsetop"),
|
|
makeRuleErrMsg("rec9", "elsemid", "elsemid", "elsebottom", "elsetop", "elsemid"),
|
|
makeRuleErrMsg("rec9", "elsebottom", "elsebottom", "elsetop", "elsemid", "elsebottom"),
|
|
makeRuleErrMsg("f0", "fn", "fn", "fn"),
|
|
makeRuleErrMsg("f1", "foo", "foo", "bar", "foo"),
|
|
makeRuleErrMsg("f1", "bar", "bar", "foo", "bar"),
|
|
makeRuleErrMsg("f2", "bar", "bar", "p[x]", "foo", "bar"),
|
|
makeRuleErrMsg("f2", "foo", "foo", "bar", "p[x]", "foo"),
|
|
makeRuleErrMsg("f2", "p[x]", "p[x]", "foo", "bar", "p[x]"),
|
|
makeRuleErrMsg("everymod", "everyp", "everyp", "everyp"),
|
|
makeRuleErrMsg("everymod", "everyq", "everyq", "everyq"),
|
|
}
|
|
|
|
result := compilerErrsToStringSlice(c.Errors)
|
|
sort.Strings(expected)
|
|
|
|
if len(result) != len(expected) {
|
|
t.Fatalf("Expected %d:\n%v\nBut got %d:\n%v", len(expected), strings.Join(expected, "\n"), len(result), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if result[i] != expected[i] {
|
|
t.Errorf("Expected %v but got: %v", expected[i], result[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompilerCheckDynamicRecursion(t *testing.T) {
|
|
// This test tries to circumvent the recursion check by using dynamic
|
|
// references. For more background info, see
|
|
// <https://github.com/open-policy-agent/opa/issues/1565>.
|
|
|
|
for _, tc := range []struct {
|
|
note, err string
|
|
mod *Module
|
|
}{
|
|
{
|
|
note: "recursion",
|
|
mod: module(`
|
|
package recursion
|
|
pkg = "recursion"
|
|
foo contains x if {
|
|
data[pkg]["foo"][x]
|
|
}
|
|
`),
|
|
err: "rego_recursion_error: rule data.recursion.foo is recursive: data.recursion.foo -> data.recursion.foo",
|
|
},
|
|
{note: "system.main",
|
|
mod: module(`
|
|
package system.main
|
|
foo if {
|
|
data[input]
|
|
}
|
|
`),
|
|
err: "rego_recursion_error: rule data.system.main.foo is recursive: data.system.main.foo -> data.system.main.foo",
|
|
},
|
|
} {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{tc.note: tc.mod}
|
|
compileStages(c, StageCheckRecursion)
|
|
|
|
result := compilerErrsToStringSlice(c.Errors)
|
|
expected := tc.err
|
|
|
|
if len(result) != 1 || result[0] != expected {
|
|
t.Errorf("Expected %v but got: %v", expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// This is a regression test for a scenario that could make recursion checking miss a recursion scenario in OPA versions older than 0.56.0.
|
|
func TestCompilerCheckPartialRuleRecursion(t *testing.T) {
|
|
// In the below policy, R2 and R3 has a recursion cycle. In OPA < 0.56.0, R1 hides this cycle from the recursion checker,
|
|
// and no error is reported.
|
|
policy := `package test
|
|
|
|
# R1
|
|
results[id] := 1 if {
|
|
id := "bar"
|
|
}
|
|
|
|
# R2
|
|
results.foo := 2 if {
|
|
final_allow
|
|
}
|
|
|
|
# R3
|
|
final_allow if {
|
|
results.foo == 3
|
|
}`
|
|
c := NewCompiler()
|
|
c.Modules = map[string]*Module{"test": module(policy)}
|
|
compileStages(c, StageCheckRecursion)
|
|
|
|
expected := Errors{
|
|
&Error{Code: "rego_recursion_error", Message: "rule data.test.results.foo is recursive: data.test.results.foo -> data.test.final_allow -> data.test.results.foo"},
|
|
&Error{Code: "rego_recursion_error", Message: "rule data.test.final_allow is recursive: data.test.final_allow -> data.test.results.foo -> data.test.final_allow"},
|
|
}
|
|
|
|
assertErrors(t, c.Errors, expected, false)
|
|
}
|
|
|
|
func TestCompilerCheckVoidCalls(t *testing.T) {
|
|
c := NewCompiler().WithCapabilities(&Capabilities{Builtins: []*Builtin{
|
|
{
|
|
Name: "test",
|
|
Decl: types.NewFunction([]types.Type{types.B}, nil),
|
|
},
|
|
}})
|
|
c.Compile(map[string]*Module{
|
|
"test.rego": module(`package test
|
|
|
|
p if {
|
|
x = test(true)
|
|
}`),
|
|
})
|
|
if !c.Failed() {
|
|
t.Fatal("expected error")
|
|
} else if c.Errors[0].Code != TypeErr || c.Errors[0].Message != "test(true) used as value" {
|
|
t.Fatal("unexpected error:", c.Errors)
|
|
}
|
|
}
|
|
|
|
func TestCompilerGetRulesExact(t *testing.T) {
|
|
mods := getCompilerTestModules()
|
|
|
|
// Add incrementally defined rules.
|
|
mods["mod-incr"] = module(`package a.b.c
|
|
|
|
p contains 1 if { true }
|
|
p contains 2 if { true }`,
|
|
)
|
|
|
|
c := NewCompiler()
|
|
c.Compile(mods)
|
|
assertNotFailed(t, c)
|
|
|
|
tests := []struct {
|
|
note string
|
|
ref any
|
|
expected []*Rule
|
|
}{
|
|
{"exact", "data.a.b.c.p", []*Rule{
|
|
c.Modules["mod-incr"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[1],
|
|
c.Modules["mod1"].Rules[0],
|
|
}},
|
|
{"too short", "data.a", []*Rule{}},
|
|
{"too long/not found", "data.a.b.c.p.q", []*Rule{}},
|
|
{"outside data", "input.a.b.c.p", []*Rule{}},
|
|
{"non-string/var", "data.a.b[data.foo]", []*Rule{}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var ref Ref
|
|
switch r := tc.ref.(type) {
|
|
case string:
|
|
ref = MustParseRef(r)
|
|
case Ref:
|
|
ref = r
|
|
}
|
|
rules := c.GetRulesExact(ref)
|
|
if len(rules) != len(tc.expected) {
|
|
t.Fatalf("Expected exactly %v rules but got: %v", len(tc.expected), rules)
|
|
}
|
|
for i := range rules {
|
|
found := slices.ContainsFunc(tc.expected, rules[i].Equal)
|
|
if !found {
|
|
t.Fatalf("Expected exactly %v but got: %v", tc.expected, rules)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerGetRulesForVirtualDocument(t *testing.T) {
|
|
mods := getCompilerTestModules()
|
|
|
|
// Add incrementally defined rules.
|
|
mods["mod-incr"] = module(`package a.b.c
|
|
|
|
p contains 1 if { true }
|
|
p contains 2 if { true }`,
|
|
)
|
|
|
|
c := NewCompiler()
|
|
c.Compile(mods)
|
|
assertNotFailed(t, c)
|
|
|
|
tests := []struct {
|
|
note string
|
|
ref any
|
|
expected []*Rule
|
|
}{
|
|
{"exact", "data.a.b.c.p", []*Rule{
|
|
c.Modules["mod-incr"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[1],
|
|
c.Modules["mod1"].Rules[0],
|
|
}},
|
|
{"deep", "data.a.b.c.p.q", []*Rule{
|
|
c.Modules["mod-incr"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[1],
|
|
c.Modules["mod1"].Rules[0],
|
|
}},
|
|
{"too short", "data.a", []*Rule{}},
|
|
{"non-existent", "data.a.deadbeef", []*Rule{}},
|
|
{"non-string/var", "data.a.b[data.foo]", []*Rule{}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var ref Ref
|
|
switch r := tc.ref.(type) {
|
|
case string:
|
|
ref = MustParseRef(r)
|
|
case Ref:
|
|
ref = r
|
|
}
|
|
rules := c.GetRulesForVirtualDocument(ref)
|
|
if len(rules) != len(tc.expected) {
|
|
t.Fatalf("Expected exactly %v rules but got: %v", len(tc.expected), rules)
|
|
}
|
|
for i := range rules {
|
|
found := slices.ContainsFunc(tc.expected, rules[i].Equal)
|
|
if !found {
|
|
t.Fatalf("Expected exactly %v but got: %v", tc.expected, rules)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerGetRulesWithPrefix(t *testing.T) {
|
|
mods := getCompilerTestModules()
|
|
|
|
// Add incrementally defined rules.
|
|
mods["mod-incr"] = module(`package a.b.c
|
|
|
|
p contains 1 if { true }
|
|
p contains 2 if { true }
|
|
q contains 3 if { true }`,
|
|
)
|
|
|
|
c := NewCompiler()
|
|
c.Compile(mods)
|
|
assertNotFailed(t, c)
|
|
|
|
tests := []struct {
|
|
note string
|
|
ref any
|
|
expected []*Rule
|
|
}{
|
|
{"exact", "data.a.b.c.p", []*Rule{
|
|
c.Modules["mod-incr"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[1],
|
|
c.Modules["mod1"].Rules[0],
|
|
}},
|
|
{"too deep", "data.a.b.c.p.q", []*Rule{}},
|
|
{"prefix", "data.a.b.c", []*Rule{
|
|
c.Modules["mod1"].Rules[0],
|
|
c.Modules["mod1"].Rules[1],
|
|
c.Modules["mod1"].Rules[2],
|
|
c.Modules["mod2"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[0],
|
|
c.Modules["mod-incr"].Rules[1],
|
|
c.Modules["mod-incr"].Rules[2],
|
|
}},
|
|
{"non-existent", "data.a.deadbeef", []*Rule{}},
|
|
{"non-string/var", "data.a.b[data.foo]", []*Rule{}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var ref Ref
|
|
switch r := tc.ref.(type) {
|
|
case string:
|
|
ref = MustParseRef(r)
|
|
case Ref:
|
|
ref = r
|
|
}
|
|
rules := c.GetRulesWithPrefix(ref)
|
|
if len(rules) != len(tc.expected) {
|
|
t.Fatalf("Expected exactly %v rules but got: %v", len(tc.expected), rules)
|
|
}
|
|
for i := range rules {
|
|
found := slices.ContainsFunc(tc.expected, rules[i].Equal)
|
|
if !found {
|
|
t.Fatalf("Expected %v but got: %v", tc.expected, rules)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerGetRules(t *testing.T) {
|
|
compiler := getCompilerWithParsedModules(map[string]string{
|
|
"mod1": `package a.b.c
|
|
|
|
p[x] = y if { q[x] = y }
|
|
q["a"] = 1 if { true }
|
|
q["b"] = 2 if { true }`,
|
|
})
|
|
|
|
compileStages(compiler, "")
|
|
|
|
rule1 := compiler.Modules["mod1"].Rules[0]
|
|
rule2 := compiler.Modules["mod1"].Rules[1]
|
|
rule3 := compiler.Modules["mod1"].Rules[2]
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected []*Rule
|
|
}{
|
|
{"data.a.b.c.p", []*Rule{rule1}},
|
|
{"data.a.b.c.p.x", []*Rule{rule1}},
|
|
{"data.a.b.c.q", []*Rule{rule2, rule3}},
|
|
{"data.a.b.c", []*Rule{rule1, rule2, rule3}},
|
|
{"data.a.b.d", nil},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
result := compiler.GetRules(MustParseRef(tc.input))
|
|
|
|
if len(result) != len(tc.expected) {
|
|
t.Fatalf("Expected %v but got: %v", tc.expected, result)
|
|
}
|
|
|
|
for i := range result {
|
|
found := slices.ContainsFunc(tc.expected, result[i].Equal)
|
|
if !found {
|
|
t.Fatalf("Expected %v but got: %v", tc.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerGetRulesDynamic(t *testing.T) {
|
|
compiler := getCompilerWithParsedModules(map[string]string{
|
|
"mod1": `package a.b.c.d
|
|
r1 = 1`,
|
|
"mod2": `package a.b.c.e
|
|
default r2 = false
|
|
r2 = 2`,
|
|
"mod3": `package a.b
|
|
r3 = 3`,
|
|
"hidden": `package system.hidden
|
|
r4 = 4`,
|
|
"mod4": `package b.c
|
|
r5[x] = 5 if { x := "foo" }
|
|
r5.bar = 6 if { input.x }
|
|
r5.baz = 7 if { input.y }
|
|
`,
|
|
})
|
|
|
|
compileStages(compiler, "")
|
|
|
|
rule1 := compiler.Modules["mod1"].Rules[0]
|
|
rule2d := compiler.Modules["mod2"].Rules[0]
|
|
rule2 := compiler.Modules["mod2"].Rules[1]
|
|
rule3 := compiler.Modules["mod3"].Rules[0]
|
|
rule4 := compiler.Modules["hidden"].Rules[0]
|
|
rule5 := compiler.Modules["mod4"].Rules[0]
|
|
rule5b := compiler.Modules["mod4"].Rules[1]
|
|
rule5c := compiler.Modules["mod4"].Rules[2]
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected []*Rule
|
|
excludeHidden bool
|
|
}{
|
|
{input: "data.a.b.c.d.r1", expected: []*Rule{rule1}},
|
|
{input: "data.a.b[x]", expected: []*Rule{rule1, rule2d, rule2, rule3}},
|
|
{input: "data.a.b[x].d", expected: []*Rule{rule1, rule3}},
|
|
{input: "data.a.b.c", expected: []*Rule{rule1, rule2d, rule2}},
|
|
{input: "data.a.b.d"},
|
|
{input: "data", expected: []*Rule{rule1, rule2d, rule2, rule3, rule4, rule5, rule5b, rule5c}},
|
|
{input: "data[x]", expected: []*Rule{rule1, rule2d, rule2, rule3, rule4, rule5, rule5b, rule5c}},
|
|
{input: "data[data.complex_computation].b[y]", expected: []*Rule{rule1, rule2d, rule2, rule3}},
|
|
{input: "data[x][y].c.e", expected: []*Rule{rule2d, rule2}},
|
|
{input: "data[x][y].r3", expected: []*Rule{rule3}},
|
|
{input: "data[x][y]", expected: []*Rule{rule1, rule2d, rule2, rule3, rule5, rule5b, rule5c}, excludeHidden: true}, // old behaviour of GetRulesDynamic
|
|
{input: "data.b.c", expected: []*Rule{rule5, rule5b, rule5c}},
|
|
{input: "data.b.c.r5", expected: []*Rule{rule5, rule5b, rule5c}},
|
|
{input: "data.b.c.r5.bar", expected: []*Rule{rule5, rule5b}}, // rule5 might still define a value for the "bar" key
|
|
{input: "data.b.c.r5.baz", expected: []*Rule{rule5, rule5c}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
result := compiler.GetRulesDynamicWithOpts(
|
|
MustParseRef(tc.input),
|
|
RulesOptions{IncludeHiddenModules: !tc.excludeHidden},
|
|
)
|
|
|
|
if len(result) != len(tc.expected) {
|
|
t.Fatalf("Expected %v but got: %v", tc.expected, result)
|
|
}
|
|
|
|
for i := range result {
|
|
found := slices.ContainsFunc(tc.expected, result[i].Equal)
|
|
if !found {
|
|
t.Fatalf("Expected %v but got: %v", tc.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompileCustomBuiltins(t *testing.T) {
|
|
|
|
compiler := NewCompiler().WithBuiltins(map[string]*Builtin{
|
|
"baz": {
|
|
Name: "baz",
|
|
Decl: types.NewFunction([]types.Type{types.S}, types.A),
|
|
},
|
|
"foo.bar": {
|
|
Name: "foo.bar",
|
|
Decl: types.NewFunction([]types.Type{types.S}, types.A),
|
|
},
|
|
})
|
|
|
|
compiler.Compile(map[string]*Module{
|
|
"test.rego": module(`
|
|
package test
|
|
|
|
p if { baz("x") = x }
|
|
q if { foo.bar("x") = x }
|
|
`),
|
|
})
|
|
|
|
// Ensure no type errors occur.
|
|
if compiler.Failed() {
|
|
t.Fatal("Unexpected compilation error:", compiler.Errors)
|
|
}
|
|
|
|
_, err := compiler.QueryCompiler().Compile(MustParseBody(`baz("x") = x; foo.bar("x") = x`))
|
|
if err != nil {
|
|
t.Fatal("Unexpected compilation error:", err)
|
|
}
|
|
|
|
// Ensure type errors occur.
|
|
exp1 := `rego_type_error: baz: invalid argument(s)`
|
|
exp2 := `rego_type_error: foo.bar: invalid argument(s)`
|
|
|
|
_, err = compiler.QueryCompiler().Compile(MustParseBody(`baz(1) = x; foo.bar(1) = x`))
|
|
if err == nil {
|
|
t.Fatal("Expected compilation error")
|
|
} else if !strings.Contains(err.Error(), exp1) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp1, err)
|
|
} else if !strings.Contains(err.Error(), exp2) {
|
|
t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", exp2, err)
|
|
}
|
|
|
|
compiler.Compile(map[string]*Module{
|
|
"test.rego": module(`
|
|
package test
|
|
|
|
p if { baz(1) = x } # type error
|
|
q if { foo.bar(1) = x } # type error
|
|
`),
|
|
})
|
|
|
|
assertCompilerErrorStrings(t, compiler, []string{exp1, exp2})
|
|
}
|
|
|
|
func TestCompilerLazyLoadingError(t *testing.T) {
|
|
testLoader := func(map[string]*Module) (map[string]*Module, error) {
|
|
return nil, errors.New("something went horribly wrong")
|
|
}
|
|
|
|
compiler := NewCompiler().WithModuleLoader(testLoader)
|
|
compiler.Compile(nil)
|
|
|
|
expected := Errors{
|
|
NewError(CompileErr, nil, "something went horribly wrong"),
|
|
}
|
|
|
|
if !slices.EqualFunc(expected, compiler.Errors, (*Error).Equal) {
|
|
t.Fatalf("Expected %v but got: %v", expected, compiler.Errors)
|
|
}
|
|
}
|
|
|
|
func TestCompilerLazyLoading(t *testing.T) {
|
|
|
|
mod1 := module(`package a.b.c
|
|
|
|
import data.x.z1 as z2
|
|
|
|
p = true if { q; r }
|
|
q = true if { z2 }`)
|
|
orig1 := mod1.Copy()
|
|
|
|
mod2 := module(`package a.b.c
|
|
|
|
r = true if { true }`)
|
|
orig2 := mod2.Copy()
|
|
|
|
mod3 := module(`package x
|
|
|
|
import data.foo.bar
|
|
import input.input
|
|
|
|
z1 = true if { [localvar | count(bar.baz.qux, localvar)] }`)
|
|
orig3 := mod3.Copy()
|
|
|
|
mod4 := module(`package foo.bar.baz
|
|
|
|
qux = grault if { true }`)
|
|
orig4 := mod4.Copy()
|
|
|
|
mod5 := module(`package foo.bar.baz
|
|
|
|
import data.d.e.f
|
|
|
|
deadbeef = f if { true }
|
|
grault = deadbeef if { true }`)
|
|
orig5 := mod5.Copy()
|
|
|
|
// testLoader will return 4 rounds of parsed modules.
|
|
rounds := []map[string]*Module{
|
|
{"mod1": mod1, "mod2": mod2},
|
|
{"mod3": mod3},
|
|
{"mod4": mod4},
|
|
{"mod5": mod5},
|
|
}
|
|
|
|
popts := ParserOptions{AllFutureKeywords: true}
|
|
|
|
// For each round, run checks.
|
|
tests := []func(map[string]*Module){
|
|
func(map[string]*Module) {
|
|
// first round, no modules because compiler is invoked with empty
|
|
// collection.
|
|
},
|
|
func(partial map[string]*Module) {
|
|
p := MustParseRuleWithOpts(`p = true { data.a.b.c.q; data.a.b.c.r }`, popts)
|
|
if !partial["mod1"].Rules[0].Equal(p) {
|
|
t.Errorf("Expected %v but got %v", p, partial["mod1"].Rules[0])
|
|
}
|
|
q := MustParseRuleWithOpts(`q = true { data.x.z1 }`, popts)
|
|
if !partial["mod1"].Rules[1].Equal(q) {
|
|
t.Errorf("Expected %v but got %v", q, partial["mod1"].Rules[0])
|
|
}
|
|
},
|
|
func(partial map[string]*Module) {
|
|
z1 := MustParseRuleWithOpts(`z1 = true { [localvar | count(data.foo.bar.baz.qux, localvar)] }`, popts)
|
|
if !partial["mod3"].Rules[0].Equal(z1) {
|
|
t.Errorf("Expected %v but got %v", z1, partial["mod3"].Rules[0])
|
|
}
|
|
},
|
|
func(partial map[string]*Module) {
|
|
qux := MustParseRuleWithOpts(`qux = grault { true }`, popts)
|
|
if !partial["mod4"].Rules[0].Equal(qux) {
|
|
t.Errorf("Expected %v but got %v", qux, partial["mod4"].Rules[0])
|
|
}
|
|
},
|
|
func(partial map[string]*Module) {
|
|
grault := MustParseRuleWithOpts(`qux = data.foo.bar.baz.grault { true }`, popts) // rewrite has not happened yet
|
|
f := MustParseRuleWithOpts(`deadbeef = data.d.e.f { true }`, popts)
|
|
if !partial["mod4"].Rules[0].Equal(grault) {
|
|
t.Errorf("Expected %v but got %v", grault, partial["mod4"].Rules[0])
|
|
}
|
|
if !partial["mod5"].Rules[0].Equal(f) {
|
|
t.Errorf("Expected %v but got %v", f, partial["mod5"].Rules[0])
|
|
}
|
|
},
|
|
}
|
|
|
|
round := 0
|
|
|
|
testLoader := func(modules map[string]*Module) (map[string]*Module, error) {
|
|
tests[round](modules)
|
|
if round >= len(rounds) {
|
|
return nil, nil
|
|
}
|
|
result := rounds[round]
|
|
round++
|
|
return result, nil
|
|
}
|
|
|
|
compiler := NewCompiler().WithModuleLoader(testLoader)
|
|
|
|
if compiler.Compile(nil); compiler.Failed() {
|
|
t.Fatalf("Got unexpected error from compiler: %v", compiler.Errors)
|
|
}
|
|
|
|
// Check the original modules are still untouched.
|
|
if !mod1.Equal(orig1) || !mod2.Equal(orig2) || !mod3.Equal(orig3) || !mod4.Equal(orig4) || !mod5.Equal(orig5) {
|
|
t.Errorf("Compiler lazy loading modified the original modules")
|
|
}
|
|
}
|
|
|
|
func TestCompilerWithMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
c := NewCompiler().WithMetrics(m)
|
|
mod := MustParseModuleWithOpts(testModule, ParserOptions{AllFutureKeywords: true})
|
|
|
|
c.Compile(map[string]*Module{"testMod": mod})
|
|
assertNotFailed(t, c)
|
|
|
|
if len(m.All()) == 0 {
|
|
t.Error("Expected to have metrics after compiling")
|
|
}
|
|
}
|
|
|
|
func TestCompilerWithStageAfterWithMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
c := NewCompiler().WithStageAfter(
|
|
"CheckRecursion",
|
|
CompilerStageDefinition{"MockStage", "mock_stage", func(*Compiler) *Error { return nil }},
|
|
)
|
|
|
|
c.WithMetrics(m)
|
|
|
|
mod := MustParseModuleWithOpts(testModule, ParserOptions{AllFutureKeywords: true})
|
|
|
|
c.Compile(map[string]*Module{"testMod": mod})
|
|
assertNotFailed(t, c)
|
|
|
|
if len(m.All()) == 0 {
|
|
t.Error("Expected to have metrics after compiling")
|
|
}
|
|
}
|
|
|
|
func TestCompilerBuildComprehensionIndexKeySet(t *testing.T) {
|
|
|
|
type expectedComprehension struct {
|
|
term, keys string
|
|
}
|
|
type exp map[int]expectedComprehension
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expected exp
|
|
wantDebug int
|
|
}{
|
|
{
|
|
note: "example: invert object",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
value = input[i]
|
|
keys = [j | value = input[j]]
|
|
}
|
|
`,
|
|
expected: exp{6: {
|
|
term: `[j | value = input[j]]`,
|
|
keys: `[value]`,
|
|
}},
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "example: multiple keys from body",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
v1 = input[i].v1
|
|
v2 = input[i].v2
|
|
keys = [j | v1 = input[j].v1; v2 = input[j].v2]
|
|
}
|
|
`,
|
|
expected: exp{7: {
|
|
term: `[j | v1 = input[j].v1; v2 = input[j].v2]`,
|
|
keys: `[v1, v2]`,
|
|
}},
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "example: nested comprehensions are supported",
|
|
module: `
|
|
package test
|
|
|
|
p = {x: ys |
|
|
x = input[i]
|
|
ys = {y | x = input[y]}
|
|
}
|
|
`,
|
|
expected: exp{6: {
|
|
term: `{y | x = input[y]}`,
|
|
keys: `[x]`,
|
|
}},
|
|
// there are still things going on here that'll be reported, besides successful indexing
|
|
wantDebug: 2,
|
|
},
|
|
{
|
|
note: "skip: lone comprehensions",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
[v | input[i] = v] # skip because no assignment
|
|
}`,
|
|
wantDebug: 0,
|
|
},
|
|
{
|
|
note: "skip: due to with modifier",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
v = input[i]
|
|
ks = [j | input[j] = v] with data.x as 1 # skip because of with modifier
|
|
}`,
|
|
wantDebug: 0,
|
|
},
|
|
{
|
|
note: "skip: due to negation",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
v = input[i]
|
|
a = []
|
|
not a = [j | input[j] = v] # skip due to negation
|
|
}`,
|
|
wantDebug: 0,
|
|
},
|
|
{
|
|
note: "skip: due to lack of comprehension",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
v = input[i]
|
|
}`,
|
|
wantDebug: 0, // nothing interesting to report here
|
|
},
|
|
{
|
|
note: "skip: due to unsafe comprehension body",
|
|
module: `
|
|
package test
|
|
|
|
f(x) if {
|
|
v = input[i]
|
|
ys = [y | y = x[j]] # x is not safe
|
|
}`,
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "skip: due to no candidates",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
ys = [y | y = input[j]]
|
|
}`,
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "mixed: due to nested comprehension containing candidate + indexed nested comprehension with key from rule body",
|
|
module: `
|
|
package test
|
|
|
|
p if {
|
|
x = input[i] # 'x' is a candidate for z (line 7)
|
|
y = 2 # 'y' is a candidate for z
|
|
z = [1 |
|
|
x = data.foo[j] # 'x' is an index key for z
|
|
t = [1 | data.bar[k] = y] # 'y' disqualifies indexing of z because it is nested inside a comprehension
|
|
]
|
|
}
|
|
`,
|
|
// Note: no comprehension index for line 7 (`z = [ ...`)
|
|
expected: exp{9: {
|
|
keys: `[y]`,
|
|
term: `[1 | data.bar[k] = y]`,
|
|
}},
|
|
wantDebug: 2,
|
|
},
|
|
{
|
|
note: "skip: avoid increasing runtime (func arg)",
|
|
module: `
|
|
package test
|
|
|
|
f(x) if {
|
|
y = input[x]
|
|
ys = [y | y = input[x]]
|
|
}`,
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "skip: avoid increasing runtime (head key)",
|
|
module: `
|
|
package test
|
|
|
|
p contains x if {
|
|
y = input[x]
|
|
ys = [y | y = input[x]]
|
|
}`,
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "skip: avoid increasing runtime (walk)",
|
|
module: `
|
|
package test
|
|
|
|
p contains x if {
|
|
y = input.bar[x]
|
|
ys = [y | a = input.foo; walk(a, [x, y])]
|
|
}`,
|
|
wantDebug: 1,
|
|
},
|
|
{
|
|
note: "bypass: use intermediate var to skip regression check",
|
|
module: `
|
|
package test
|
|
|
|
p contains x if {
|
|
y = input[x]
|
|
ys = [y | y = input[z]; z = x]
|
|
}`,
|
|
expected: exp{6: {
|
|
term: ` [y | y = input[z]; z = x]`,
|
|
keys: `[x, y]`,
|
|
}},
|
|
wantDebug: 1,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
dbg := bytes.Buffer{}
|
|
m := metrics.New()
|
|
compiler := NewCompiler().WithMetrics(m).WithDebug(&dbg)
|
|
mod, err := ParseModule("test.rego", tc.module)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
compiler.Compile(map[string]*Module{"test.rego": mod})
|
|
if compiler.Failed() {
|
|
t.Fatal(compiler.Errors)
|
|
}
|
|
|
|
messages := strings.Split(dbg.String(), "\n")
|
|
messages = messages[:len(messages)-1] // last one is an empty string
|
|
if exp, act := tc.wantDebug, len(messages); exp != act {
|
|
t.Errorf("expected %d debug messages, got %d", exp, act)
|
|
for i, m := range messages {
|
|
t.Logf("%d: %s\n", i, m)
|
|
}
|
|
}
|
|
|
|
n := m.Counter(compileStageComprehensionIndexBuild).Value().(uint64)
|
|
if exp, act := len(tc.expected), len(compiler.comprehensionIndices); exp != act {
|
|
t.Fatalf("expected %d indices to be built. got: %d", exp, act)
|
|
}
|
|
if len(tc.expected) == 0 {
|
|
return
|
|
}
|
|
if n == 0 {
|
|
t.Fatal("expected counter to be incremented")
|
|
}
|
|
|
|
for row, exp := range tc.expected {
|
|
var comprehension *Term
|
|
WalkTerms(compiler.Modules["test.rego"], func(x *Term) bool {
|
|
if !IsComprehension(x.Value) {
|
|
return true
|
|
}
|
|
_, ok := tc.expected[x.Location.Row]
|
|
if !ok {
|
|
return false
|
|
} else if comprehension != nil {
|
|
t.Fatal("expected at most one comprehension per line in test module")
|
|
}
|
|
comprehension = x
|
|
return false
|
|
})
|
|
if comprehension == nil {
|
|
t.Fatal("expected comprehension at line:", row)
|
|
}
|
|
|
|
result := compiler.ComprehensionIndex(comprehension)
|
|
if result == nil {
|
|
t.Fatal("expected result")
|
|
}
|
|
|
|
expTerm := MustParseTerm(exp.term)
|
|
if !result.Term.Equal(expTerm) {
|
|
t.Fatalf("expected term to be %v but got: %v", expTerm, result.Term)
|
|
}
|
|
|
|
expKeys := MustParseTerm(exp.keys).Value.(*Array)
|
|
if NewArray(result.Keys...).Compare(expKeys) != 0 {
|
|
t.Fatalf("expected keys to be %v but got: %v", expKeys, result.Keys)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerBuildRequiredCapabilities(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
opts CompileOpts
|
|
builtins []string
|
|
features []string
|
|
keywords []string
|
|
}{
|
|
{
|
|
note: "trivial v0",
|
|
module: `
|
|
package x
|
|
|
|
p { input > 7 }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
builtins: []string{"eq", "gt"},
|
|
},
|
|
{
|
|
note: "trivial v1",
|
|
module: `
|
|
package x
|
|
|
|
p if { input > 7 }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"eq", "gt"},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "rego.v1 import, v0 module",
|
|
module: `
|
|
package x
|
|
|
|
import rego.v1
|
|
|
|
p if { true }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
features: []string{"rego_v1_import"},
|
|
},
|
|
{
|
|
note: "rego.v1 import, v1 module",
|
|
module: `
|
|
package x
|
|
|
|
import rego.v1
|
|
|
|
p if { true }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "rego.v1 import, default rego-version module (v1)",
|
|
module: `
|
|
package x
|
|
|
|
import rego.v1
|
|
|
|
p if { true }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "future.keywords wildcard, v0 module",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
keywords: []string{"contains", "every", "if", "in", "not"},
|
|
},
|
|
{
|
|
note: "future.keywords wildcard, v1 module",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
features: []string{"rego_v1"},
|
|
keywords: []string{"not"},
|
|
},
|
|
{
|
|
note: "future.keywords wildcard, default rego-version module (v1)",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords
|
|
`,
|
|
features: []string{"rego_v1"},
|
|
keywords: []string{"not"},
|
|
},
|
|
{
|
|
note: "future.keywords specific, v0 module",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords.in
|
|
import future.keywords.if
|
|
import future.keywords.contains
|
|
import future.keywords.every
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
keywords: []string{"contains", "every", "if", "in"},
|
|
},
|
|
{
|
|
note: "future.keywords specific, v1 module",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords.in
|
|
import future.keywords.if
|
|
import future.keywords.contains
|
|
import future.keywords.every
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "future.keywords specific, default rego-version module (v1)",
|
|
module: `
|
|
package x
|
|
|
|
import future.keywords.in
|
|
import future.keywords.if
|
|
import future.keywords.contains
|
|
import future.keywords.every
|
|
`,
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "rewriting erases assignment",
|
|
module: `
|
|
package x
|
|
|
|
p if { a := 7 }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"assign", "eq"},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "rewriting erases equals",
|
|
module: `
|
|
package x
|
|
|
|
p if { input == 7 }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"eq", "equal"},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "rewriting erases print",
|
|
module: `
|
|
package x
|
|
|
|
p if { print(7) }
|
|
`,
|
|
opts: CompileOpts{EnablePrintStatements: true, ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"eq", "internal.print", "print"},
|
|
features: []string{"rego_v1"},
|
|
},
|
|
|
|
{
|
|
note: "rewriting erases print but disabled",
|
|
module: `
|
|
package x
|
|
|
|
p if { print(7) }
|
|
`,
|
|
opts: CompileOpts{EnablePrintStatements: false, ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"print"}, // only print required because compiler will replace with true
|
|
features: []string{"rego_v1"},
|
|
},
|
|
{
|
|
note: "dots in the head, v0 module",
|
|
module: `
|
|
package x
|
|
|
|
a.b.c := 7
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
features: []string{"rule_head_ref_string_prefixes"},
|
|
},
|
|
{
|
|
note: "dots in the head, v1 module",
|
|
module: `
|
|
package x
|
|
|
|
a.b.c := 7
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
features: []string{"rego_v1"}, // rego_v1 includes rule_head_ref_string_prefixes
|
|
},
|
|
{
|
|
note: "dots in the head, default rego-version module (v1)",
|
|
module: `
|
|
package x
|
|
|
|
a.b.c := 7
|
|
`,
|
|
features: []string{"rego_v1"}, // rego_v1 includes rule_head_ref_string_prefixes
|
|
},
|
|
{
|
|
note: "dynamic dots in the head, v0 module",
|
|
module: `
|
|
package x
|
|
|
|
a[x].c[y] := z { x := "b"; y := "c"; z := "d" }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV0}},
|
|
builtins: []string{"assign", "eq"},
|
|
features: []string{"rule_head_refs"},
|
|
},
|
|
{
|
|
note: "dynamic dots in the head, v1 module",
|
|
module: `
|
|
package x
|
|
|
|
a[x].c[y] := z if { x := "b"; y := "c"; z := "d" }
|
|
`,
|
|
opts: CompileOpts{ParserOptions: ParserOptions{RegoVersion: RegoV1}},
|
|
builtins: []string{"assign", "eq"},
|
|
features: []string{"rego_v1"}, // rego_v1 includes rule_head_refs
|
|
},
|
|
{
|
|
note: "dynamic dots in the head, default rego-version module (v1)",
|
|
module: `
|
|
package x
|
|
|
|
a[x].c[y] := z if { x := "b"; y := "c"; z := "d" }
|
|
`,
|
|
builtins: []string{"assign", "eq"},
|
|
features: []string{"rego_v1"}, // rego_v1 includes rule_head_refs
|
|
},
|
|
{
|
|
note: "template-string",
|
|
module: `package test
|
|
p := $"foo {42}"`,
|
|
builtins: []string{"internal.template_string"},
|
|
features: []string{FeatureRegoV1, FeatureTemplateStrings},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := MustCompileModulesWithOpts(map[string]string{
|
|
"test.rego": tc.module,
|
|
}, tc.opts)
|
|
|
|
var names []string
|
|
for i := range compiler.Required.Builtins {
|
|
names = append(names, compiler.Required.Builtins[i].Name)
|
|
}
|
|
|
|
if !slices.Equal(names, tc.builtins) {
|
|
t.Fatalf("expected builtins to be %v but got %v", tc.builtins, names)
|
|
}
|
|
|
|
if !slices.Equal(compiler.Required.FutureKeywords, tc.keywords) {
|
|
t.Fatalf("expected keywords to be %v but got %v", tc.keywords, compiler.Required.FutureKeywords)
|
|
}
|
|
|
|
if !slices.Equal(compiler.Required.Features, tc.features) {
|
|
t.Fatalf("expected features to be %v but got %v", tc.features, compiler.Required.Features)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerAllowMultipleAssignments(t *testing.T) {
|
|
|
|
_, err := CompileModules(map[string]string{"test.rego": `
|
|
package test
|
|
|
|
p := 7
|
|
p := 8
|
|
`})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestQueryCompiler(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
q string
|
|
pkg string
|
|
imports []string
|
|
input string
|
|
regoVersion RegoVersion
|
|
expected any
|
|
}{
|
|
{
|
|
note: "empty query",
|
|
q: " \t \n # foo \n",
|
|
expected: errors.New("1 error occurred: rego_compile_error: empty query cannot be compiled"),
|
|
},
|
|
{
|
|
note: "invalid eq",
|
|
q: "eq()",
|
|
expected: errors.New("1 error occurred: 1:1: rego_type_error: eq: arity mismatch\n\thave: ()\n\twant: (any, any)"),
|
|
},
|
|
{
|
|
note: "invalid eq",
|
|
q: "eq(1)",
|
|
expected: errors.New("1 error occurred: 1:1: rego_type_error: eq: arity mismatch\n\thave: (number)\n\twant: (any, any)"),
|
|
},
|
|
{
|
|
note: "rewrite assignment",
|
|
q: "a := 1; [b, c] := data.foo",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: "__localq0__ = 1; [__localq1__, __localq2__] = data.foo",
|
|
},
|
|
{
|
|
note: "exports resolved",
|
|
q: "z",
|
|
pkg: `package a.b.c`,
|
|
imports: nil,
|
|
expected: "data.a.b.c.z",
|
|
},
|
|
{
|
|
note: "imports resolved",
|
|
q: "z",
|
|
pkg: `package a.b.c.d`,
|
|
imports: []string{"import data.a.b.c.z"},
|
|
expected: "data.a.b.c.z",
|
|
},
|
|
{
|
|
note: "rewrite comprehensions",
|
|
q: "[x[i] | a = [[1], [2]]; x = a[j]]",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: "[__localq0__ | a = [[1], [2]]; x = a[j]; __localq0__ = x[i]]",
|
|
},
|
|
{
|
|
note: "unsafe vars",
|
|
q: "z",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:1: rego_unsafe_var_error: var z is unsafe"),
|
|
},
|
|
{
|
|
note: "unsafe var that is a future keyword",
|
|
q: "1 in 2",
|
|
expected: errors.New("1 error occurred: 1:3: rego_unsafe_var_error: var in is unsafe (hint: `import future.keywords.in` to import a future keyword)"),
|
|
regoVersion: RegoV0,
|
|
},
|
|
{
|
|
note: "unsafe declared var",
|
|
q: "[1 | some x; x == 1]",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:14: rego_unsafe_var_error: var x is unsafe"),
|
|
},
|
|
{
|
|
note: "safe vars",
|
|
q: `data; abc`,
|
|
pkg: `package ex`,
|
|
imports: []string{"import input.xyz as abc"},
|
|
expected: `data; input.xyz`,
|
|
},
|
|
{
|
|
note: "reorder",
|
|
q: `x != 1; x = 0`,
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: `x = 0; x != 1`,
|
|
},
|
|
{
|
|
note: "bad with target",
|
|
q: "x = 1 with foo.p as null",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:12: rego_type_error: with keyword target must reference existing input, data, or a function"),
|
|
},
|
|
{
|
|
note: "rewrite with value",
|
|
q: `1 with input as [z]`,
|
|
pkg: "package a.b.c",
|
|
imports: nil,
|
|
expected: `__localq1__ = data.a.b.c.z; __localq0__ = [__localq1__]; 1 with input as __localq0__`,
|
|
},
|
|
{
|
|
note: "built-in function arity mismatch",
|
|
q: `startswith("x")`,
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:1: rego_type_error: startswith: arity mismatch\n\thave: (string)\n\twant: (search: string, base: string)"),
|
|
},
|
|
{
|
|
note: "built-in function arity mismatch (arity 0)",
|
|
q: `x := opa.runtime("foo")`,
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:6: rego_type_error: opa.runtime: arity mismatch\n\thave: (string, ???)\n\twant: ()"),
|
|
},
|
|
{
|
|
note: "built-in function arity mismatch, nested",
|
|
q: "count(sum())",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("1 error occurred: 1:7: rego_type_error: sum: arity mismatch\n\thave: (???)\n\twant: (collection: any<array[number], set[number]>)"),
|
|
},
|
|
{
|
|
note: "check types",
|
|
q: "x = data.a.b.c.z; y = null; x = y",
|
|
pkg: "",
|
|
imports: nil,
|
|
expected: errors.New("match error\n\tleft : number\n\tright : null"),
|
|
},
|
|
{
|
|
note: "undefined function",
|
|
q: "data.deadbeef(x)",
|
|
expected: errors.New("rego_type_error: undefined function data.deadbeef"),
|
|
},
|
|
{
|
|
note: "imports resolved without package",
|
|
q: "abc",
|
|
pkg: "",
|
|
imports: []string{"import input.xyz as abc"},
|
|
expected: "input.xyz",
|
|
},
|
|
{
|
|
note: "void call used as value",
|
|
q: "x = print(1)",
|
|
expected: errors.New("rego_type_error: print(1) used as value"),
|
|
},
|
|
{
|
|
note: "print call erasure",
|
|
q: `print(1)`,
|
|
expected: "true",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
popts := ParserOptions{RegoVersion: tc.regoVersion}
|
|
t.Run(tc.note, runQueryCompilerTest(tc.q, popts, tc.pkg, tc.imports, tc.expected))
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerRewrittenVars(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
q string
|
|
vars map[string]string
|
|
}{
|
|
{"assign", "a := 1", map[string]string{"__localq0__": "a"}},
|
|
{"suppress only seen", "b = 1; a := b", map[string]string{"__localq0__": "a"}},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Compile(nil)
|
|
assertNotFailed(t, c)
|
|
qc := c.QueryCompiler()
|
|
body, err := ParseBody(tc.q)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = qc.Compile(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vars := qc.RewrittenVars()
|
|
if len(tc.vars) != len(vars) {
|
|
t.Fatalf("Expected %v but got: %v", tc.vars, vars)
|
|
}
|
|
for k := range vars {
|
|
if vars[k] != Var(tc.vars[string(k)]) {
|
|
t.Fatalf("Expected %v but got: %v", tc.vars, vars)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerRecompile(t *testing.T) {
|
|
|
|
// Query which contains terms that will be rewritten.
|
|
parsed := MustParseBody(`a := [1]; data.bar == data.foo[a[0]]`)
|
|
parsed0 := parsed
|
|
|
|
qc := NewCompiler().QueryCompiler()
|
|
compiled, err := qc.Compile(parsed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
compiled2, err := qc.Compile(parsed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if !compiled2.Equal(compiled) {
|
|
t.Fatalf("Expected same compiled query. Expected: %v, Got: %v", compiled, compiled2)
|
|
}
|
|
|
|
if !parsed0.Equal(parsed) {
|
|
t.Fatalf("Expected parsed query to be unmodified. Expected %v, Got: %v", parsed0, parsed)
|
|
}
|
|
|
|
}
|
|
|
|
func TestQueryCompilerWithMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
c := NewCompiler().WithMetrics(m)
|
|
c.Compile(getCompilerTestModules())
|
|
assertNotFailed(t, c)
|
|
m.Clear()
|
|
|
|
qc := c.QueryCompiler()
|
|
|
|
query := MustParseBody("a = 1; a > 2")
|
|
_, err := qc.Compile(query)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from %v: %v", query, err)
|
|
}
|
|
|
|
if len(m.All()) == 0 {
|
|
t.Error("Expected to have metrics after compiling")
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerWithStageAfterWithMetrics(t *testing.T) {
|
|
m := metrics.New()
|
|
c := NewCompiler().WithMetrics(m)
|
|
c.Compile(getCompilerTestModules())
|
|
assertNotFailed(t, c)
|
|
m.Clear()
|
|
|
|
qc := c.QueryCompiler().WithStageAfter(
|
|
"CheckSafety",
|
|
QueryCompilerStageDefinition{
|
|
"MockStage",
|
|
"mock_stage",
|
|
func(_ QueryCompiler, b Body) (Body, error) {
|
|
return b, nil
|
|
},
|
|
})
|
|
|
|
query := MustParseBody("a = 1; a > 2")
|
|
_, err := qc.Compile(query)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from %v: %v", query, err)
|
|
}
|
|
|
|
if len(m.All()) == 0 {
|
|
t.Error("Expected to have metrics after compiling")
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerWithUnsafeBuiltins(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
query string
|
|
compiler *Compiler
|
|
opts func(QueryCompiler) QueryCompiler
|
|
err string
|
|
}{
|
|
{
|
|
note: "builtin unsafe via compiler",
|
|
query: "count([])",
|
|
compiler: NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
err: "unsafe built-in function calls in expression: count",
|
|
},
|
|
{
|
|
note: "builtin unsafe via query compiler",
|
|
query: "count([])",
|
|
compiler: NewCompiler(),
|
|
opts: func(qc QueryCompiler) QueryCompiler {
|
|
return qc.WithUnsafeBuiltins(map[string]struct{}{"count": {}})
|
|
},
|
|
err: "unsafe built-in function calls in expression: count",
|
|
},
|
|
{
|
|
note: "builtin unsafe via compiler, 'with' mocking",
|
|
query: "is_array([]) with is_array as count",
|
|
compiler: NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"count": {}}),
|
|
err: `with keyword replacing built-in function: target must not be unsafe: "count"`,
|
|
},
|
|
{
|
|
note: "builtin unsafe via query compiler, 'with' mocking",
|
|
query: "is_array([]) with is_array as count",
|
|
compiler: NewCompiler(),
|
|
opts: func(qc QueryCompiler) QueryCompiler {
|
|
return qc.WithUnsafeBuiltins(map[string]struct{}{"count": {}})
|
|
},
|
|
err: `with keyword replacing built-in function: target must not be unsafe: "count"`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
qc := tc.compiler.QueryCompiler()
|
|
if tc.opts != nil {
|
|
qc = tc.opts(qc)
|
|
}
|
|
_, err := qc.Compile(MustParseBody(tc.query))
|
|
var errs Errors
|
|
if !errors.As(err, &errs) {
|
|
t.Fatalf("expected error type %T, got %v %[2]T", errs, err)
|
|
}
|
|
if exp, act := 1, len(errs); exp != act {
|
|
t.Fatalf("expected %d error(s), got %d", exp, act)
|
|
}
|
|
if exp, act := tc.err, errs[0].Message; exp != act {
|
|
t.Errorf("expected message %q, got %q", exp, act)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerWithDeprecatedBuiltins(t *testing.T) {
|
|
cases := []strictnessQueryTestCase{
|
|
{
|
|
note: "all() built-in",
|
|
query: "all([true, false])",
|
|
expectedErrors: errors.New("1 error occurred: 1:1: rego_type_error: deprecated built-in function calls in expression: all"),
|
|
},
|
|
{
|
|
note: "any() built-in",
|
|
query: "any([true, false])",
|
|
expectedErrors: errors.New("1 error occurred: 1:1: rego_type_error: deprecated built-in function calls in expression: any"),
|
|
},
|
|
}
|
|
|
|
runStrictnessQueryTestCase(t, cases)
|
|
}
|
|
|
|
func TestQueryCompilerWithUnusedAssignedVar(t *testing.T) {
|
|
cases := []strictnessQueryTestCase{
|
|
{
|
|
note: "array comprehension",
|
|
query: "[1 | x := 2]",
|
|
expectedErrors: errors.New("1 error occurred: 1:6: rego_compile_error: assigned var x unused"),
|
|
},
|
|
{
|
|
note: "set comprehension",
|
|
query: "{1 | x := 2}",
|
|
expectedErrors: errors.New("1 error occurred: 1:6: rego_compile_error: assigned var x unused"),
|
|
},
|
|
{
|
|
note: "object comprehension",
|
|
query: "{1: 2 | x := 2}",
|
|
expectedErrors: errors.New("1 error occurred: 1:9: rego_compile_error: assigned var x unused"),
|
|
},
|
|
{
|
|
note: "every: unused var in body",
|
|
query: "every _ in [] { x := 10 }",
|
|
expectedErrors: errors.New("1 error occurred: 1:17: rego_compile_error: assigned var x unused"),
|
|
},
|
|
}
|
|
|
|
runStrictnessQueryTestCase(t, cases)
|
|
}
|
|
|
|
func TestQueryCompilerCheckKeywordOverrides(t *testing.T) {
|
|
cases := []strictnessQueryTestCase{
|
|
{
|
|
note: "input assigned",
|
|
query: "input := 1",
|
|
expectedErrors: errors.New("1 error occurred: 1:1: rego_compile_error: variables must not shadow input (use a different variable name)"),
|
|
},
|
|
{
|
|
note: "data assigned",
|
|
query: "data := 1",
|
|
expectedErrors: errors.New("1 error occurred: 1:1: rego_compile_error: variables must not shadow data (use a different variable name)"),
|
|
},
|
|
{
|
|
note: "nested input assigned",
|
|
query: "d := [input | input := 1]",
|
|
expectedErrors: errors.New("1 error occurred: 1:15: rego_compile_error: variables must not shadow input (use a different variable name)"),
|
|
},
|
|
}
|
|
|
|
runStrictnessQueryTestCase(t, cases)
|
|
}
|
|
|
|
type strictnessQueryTestCase struct {
|
|
note string
|
|
query string
|
|
expectedErrors error
|
|
}
|
|
|
|
func runStrictnessQueryTestCase(t *testing.T, cases []strictnessQueryTestCase) {
|
|
t.Helper()
|
|
makeTestRunner := func(tc strictnessQueryTestCase, strict bool) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
c := NewCompiler().WithStrict(strict)
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
result, err := c.QueryCompiler().Compile(MustParseBodyWithOpts(tc.query, opts))
|
|
|
|
if strict {
|
|
if err == nil {
|
|
t.Fatalf("Expected error from %v but got: %v", tc.query, result)
|
|
}
|
|
if !strings.Contains(err.Error(), tc.expectedErrors.Error()) {
|
|
t.Fatalf("Expected error %v but got: %v", tc.expectedErrors, err)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error from %v: %v", tc.query, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note+"_strict", makeTestRunner(tc, true))
|
|
t.Run(tc.note+"_non-strict", makeTestRunner(tc, false))
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerRewriteTemplateStrings(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
query string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "empty template string",
|
|
query: `$""`,
|
|
exp: `internal.template_string([""], __localq0__); __localq0__`,
|
|
},
|
|
{
|
|
note: "non-empty template string with no expressions",
|
|
query: `$"foo"`,
|
|
exp: `internal.template_string(["foo"], __localq0__); __localq0__`,
|
|
},
|
|
{
|
|
note: "template string with value expressions",
|
|
query: `$"{true} {null} {42} {1.2} {"foo"} {[1, 2]} {{1, 2}} {{"a": 1, "b": 2}}"`,
|
|
exp: `__localq4__ = {__localq0__ | __localq0__ = [1, 2]}
|
|
__localq5__ = {__localq1__ | __localq1__ = {1, 2}}
|
|
__localq6__ = {__localq2__ | __localq2__ = {"a": 1, "b": 2}}
|
|
internal.template_string([true, " ", null, " ", 42, " ", 1.2, " ", "foo", " ", __localq4__, " ", __localq5__, " ", __localq6__], __localq3__)
|
|
__localq3__`,
|
|
},
|
|
{
|
|
note: "template string with var and ref expressions",
|
|
query: `x := 42; $"{x} {input.y}"`,
|
|
exp: `__localq0__ = 42
|
|
__localq3__ = {__localq1__ | __localq1__ = input.y}
|
|
internal.template_string([{__localq0__}, " ", __localq3__], __localq2__)
|
|
__localq2__`,
|
|
},
|
|
{
|
|
note: "template string with call expressions",
|
|
query: `$"{array.concat([1], [2])} {1 + 2} {true != false}"`,
|
|
exp: `__localq7__ = {__localq0__ | array.concat([1], [2], __localq3__); __localq0__ = __localq3__}
|
|
__localq8__ = {__localq1__ | plus(1, 2, __localq4__); __localq1__ = __localq4__}
|
|
__localq9__ = {__localq2__ | neq(true, false, __localq5__); __localq2__ = __localq5__}
|
|
internal.template_string([__localq7__, " ", __localq8__, " ", __localq9__], __localq6__)
|
|
__localq6__`,
|
|
},
|
|
{
|
|
note: "template string with comprehension expressions",
|
|
query: `$"{[x | x := input.xs[_]]} {{y | y := input.ys[_]}} {{a: b | a := input.as[_]; b := input.bs[_]}}"`,
|
|
exp: `__localq8__ = {__localq4__ | __localq4__ = [__localq0__ | __localq0__ = input.xs[_]]}
|
|
__localq9__ = {__localq5__ | __localq5__ = {__localq1__ | __localq1__ = input.ys[_]}}
|
|
__localq10__ = {__localq6__ | __localq6__ = {__localq2__: __localq3__ | __localq2__ = input["as"][_]; __localq3__ = input.bs[_]}}
|
|
internal.template_string([__localq8__, " ", __localq9__, " ", __localq10__], __localq7__)
|
|
__localq7__`,
|
|
},
|
|
{
|
|
note: "binding",
|
|
query: `x := 42; y := $"{x}"`,
|
|
exp: `__localq0__ = 42
|
|
internal.template_string([{__localq0__}], __localq2__);
|
|
__localq1__ = __localq2__`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
qc := c.QueryCompiler()
|
|
|
|
result, err := qc.Compile(MustParseBody(tc.query))
|
|
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
exp := MustParseBody(tc.exp)
|
|
|
|
if !exp.Equal(result) {
|
|
t.Fatalf("Expected:\n%v\n\nGot:\n%v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerRewriteTemplateStringsErrors(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
query string
|
|
expErr string
|
|
}{
|
|
{
|
|
note: "unsafe var",
|
|
query: `$"{x}"`,
|
|
expErr: "rego_unsafe_var_error: var x is unsafe",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
qc := c.QueryCompiler()
|
|
_, err := qc.Compile(MustParseBody(tc.query))
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected error but got none")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), tc.expErr) {
|
|
t.Fatalf("Expected error %v but got: %v", tc.expErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertCompilerErrorStrings(t *testing.T, compiler *Compiler, expected []string) {
|
|
t.Helper()
|
|
result := compilerErrsToStringSlice(compiler.Errors)
|
|
|
|
if len(result) != len(expected) {
|
|
t.Fatalf("Expected %d:\n%v\nBut got %d:\n%v", len(expected), strings.Join(expected, "\n"), len(result), strings.Join(result, "\n"))
|
|
}
|
|
for i := range result {
|
|
if !strings.Contains(result[i], expected[i]) {
|
|
t.Errorf("Expected %v but got: %v", expected[i], result[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertNotFailed(t *testing.T, c *Compiler) {
|
|
t.Helper()
|
|
if c.Failed() {
|
|
t.Fatalf("Unexpected compilation error: %v", c.Errors)
|
|
}
|
|
}
|
|
|
|
func getCompilerWithParsedModules(mods map[string]string) *Compiler {
|
|
|
|
parsed := map[string]*Module{}
|
|
|
|
for id, input := range mods {
|
|
mod, err := ParseModule(id, input)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
parsed[id] = mod
|
|
}
|
|
|
|
compiler := NewCompiler()
|
|
compiler.Modules = parsed
|
|
|
|
return compiler
|
|
}
|
|
|
|
// compileStages is a helper function to run compiler up to a given stage.
|
|
// If stageID is empty, a normal full compile run is performed.
|
|
// This works directly on c.Modules that are already set by tests.
|
|
func compileStages(c *Compiler, stageID StageID) {
|
|
c.init()
|
|
|
|
c.sorted = make([]string, 0, len(c.Modules))
|
|
for name := range c.Modules {
|
|
c.sorted = append(c.sorted, name)
|
|
}
|
|
sort.Strings(c.sorted)
|
|
|
|
c = c.SetErrorLimit(0) // Tests need to see all errors, not just the first few
|
|
|
|
if stageID != "" {
|
|
c = c.WithOnlyStagesUpTo(stageID)
|
|
}
|
|
|
|
c.compile()
|
|
}
|
|
|
|
func getCompilerTestModules() map[string]*Module {
|
|
|
|
mod1 := MustParseModule(`package a.b.c
|
|
import rego.v1
|
|
import data.x.y.z as foo
|
|
import data.g.h.k
|
|
|
|
p contains x if { q[x]; not r[x] }
|
|
q contains x if { foo[i] = x }
|
|
z = 400 if { true }`,
|
|
)
|
|
|
|
mod2 := MustParseModule(`package a.b.c
|
|
import rego.v1
|
|
import data.bar
|
|
import data.x.y.p
|
|
|
|
r contains x if { bar[x] = 100; p = 101 }`)
|
|
|
|
mod3 := MustParseModule(`package a.b.d
|
|
import rego.v1
|
|
import input.x as y
|
|
|
|
t = true if { input = {y.secret: [{y.keyid}]} }
|
|
x = false if { true }`)
|
|
|
|
mod4 := MustParseModule(`package a.b.empty`)
|
|
|
|
mod5 := MustParseModule(`package a.b.compr
|
|
import rego.v1
|
|
import input.x as y
|
|
import data.a.b.c.q
|
|
|
|
p = true if { [y.a | true] }
|
|
r = true if { [q.a | true] }
|
|
s = true if { [true | y.a = 0] }
|
|
t = true if { [true | q[i] = 1] }
|
|
u = true if { [true | _ = [y.a | true]] }
|
|
v = true if { [true | _ = [true | q[i] = 1]] }
|
|
`,
|
|
)
|
|
|
|
mod6 := MustParseModule(`package a.b.nested
|
|
import rego.v1
|
|
import data.x
|
|
import data.z
|
|
import input.x as y
|
|
|
|
p = true if { x[y[i].a[z.b[j]]] }
|
|
q = true if { x = v; v[y[i]] }
|
|
r = 1 if { true }
|
|
s = true if { x[r] }`,
|
|
)
|
|
|
|
mod7 := MustParseModule(`package a.b.funcs
|
|
import rego.v1
|
|
fn(x) = y if {
|
|
trim(x, ".", y)
|
|
}
|
|
|
|
bar([x, y]) = [a, [b, c]] if {
|
|
fn(x, a)
|
|
y[1].b = b
|
|
y[i].a = "hi"
|
|
c = y[i].b
|
|
}
|
|
|
|
foorule = true if {
|
|
bar(["hi.there", [{"a": "hi", "b": 1}, {"a": "bye", "b": 0}]], [a, [b, c]])
|
|
}`)
|
|
|
|
return map[string]*Module{
|
|
"mod1": mod1,
|
|
"mod2": mod2,
|
|
"mod3": mod3,
|
|
"mod4": mod4,
|
|
"mod5": mod5,
|
|
"mod6": mod6,
|
|
"mod7": mod7,
|
|
}
|
|
}
|
|
|
|
func compilerErrsToStringSlice(errors []*Error) []string {
|
|
result := make([]string, 0, len(errors))
|
|
for _, e := range errors {
|
|
msg := strings.SplitN(e.Error(), ":", 3)[2]
|
|
result = append(result, strings.TrimSpace(msg))
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
func runQueryCompilerTest(q string, popts ParserOptions, pkg string, imports []string, expected any) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
t.Helper()
|
|
c := NewCompiler().WithEnablePrintStatements(false)
|
|
c.Compile(getCompilerTestModules())
|
|
assertNotFailed(t, c)
|
|
qc := c.QueryCompiler()
|
|
query := MustParseBodyWithOpts(q, popts)
|
|
var qctx *QueryContext
|
|
|
|
if pkg != "" {
|
|
qctx = qctx.WithPackage(MustParsePackage(pkg))
|
|
}
|
|
if len(imports) != 0 {
|
|
qctx = qctx.WithImports(MustParseImports(strings.Join(imports, "\n")))
|
|
}
|
|
|
|
if qctx != nil {
|
|
qc.WithContext(qctx)
|
|
}
|
|
|
|
switch expected := expected.(type) {
|
|
case string:
|
|
expectedQuery := MustParseBody(expected)
|
|
result, err := qc.Compile(query)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from %v: %v", query, err)
|
|
}
|
|
if !expectedQuery.Equal(result) {
|
|
t.Fatalf("Expected:\n%v\n\nGot:\n%v", expectedQuery, result)
|
|
}
|
|
case error:
|
|
result, err := qc.Compile(query)
|
|
if err == nil {
|
|
t.Fatalf("Expected error from %v but got: %v", query, result)
|
|
}
|
|
if !strings.Contains(err.Error(), expected.Error()) {
|
|
t.Fatalf("Expected error %v but got: %v", expected, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompilerCapabilitiesFeatures(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
module string
|
|
features []string
|
|
builtins []*Builtin
|
|
expectedErr string
|
|
}{
|
|
{
|
|
note: "no features, no ref-head rules",
|
|
module: `package test
|
|
p := 42`,
|
|
},
|
|
{
|
|
note: "no features, ref-head rule",
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
expectedErr: "rego_compile_error: rule heads with refs are not supported: p.q.r",
|
|
},
|
|
{
|
|
note: "no features, general-ref-head rule",
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
expectedErr: "rego_compile_error: rule heads with refs are not supported: p[q].r[s]",
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head feature, no ref-head rules",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
},
|
|
module: `package test
|
|
p := 42`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head feature, ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
},
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
},
|
|
{
|
|
note: "ref-head feature, ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
},
|
|
{
|
|
note: "rego-v1 feature, ref-head rule",
|
|
features: []string{
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head feature, general-ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
},
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
expectedErr: "rego_type_error: rule heads with general refs (containing variables) are not supported: p[q].r[s]",
|
|
},
|
|
{
|
|
note: "ref-head feature, general-ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
},
|
|
{
|
|
note: "rego-v1 feature, general-ref-head rule",
|
|
features: []string{
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head & ref-head features, general-ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head & ref-head & rego-v1 features, general-ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
FeatureRefHeads,
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
p[q].r[s] := 42 if { q := "foo"; s := "bar" }`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head & ref-head features, ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head & ref-head & rego-v1 features, ref-head rule",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
FeatureRefHeads,
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
p.q.r := 42`,
|
|
},
|
|
{
|
|
note: "no features, string-prefix-ref-head with contains kw",
|
|
features: []string{},
|
|
module: `package test
|
|
import future.keywords.contains
|
|
p.x contains 1`,
|
|
expectedErr: "rego_compile_error: rule heads with refs are not supported: p.x",
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head feature, string-prefix-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
},
|
|
module: `package test
|
|
import future.keywords.contains
|
|
p.x contains 1`,
|
|
},
|
|
{
|
|
note: "ref-head feature, string-prefix-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
import future.keywords.contains
|
|
p.x contains 1`,
|
|
},
|
|
{
|
|
note: "rego-v1 feature, string-prefix-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
import future.keywords.contains
|
|
p.x contains 1`,
|
|
},
|
|
|
|
{
|
|
note: "no features, general-ref-head with contains kw",
|
|
features: []string{},
|
|
module: `package test
|
|
import future.keywords
|
|
p[x] contains 1 if x = "foo"`,
|
|
expectedErr: "rego_compile_error: rule heads with refs are not supported: p[x]",
|
|
},
|
|
{
|
|
note: "string-prefix-ref-head feature, general-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRefHeadStringPrefixes,
|
|
},
|
|
module: `package test
|
|
import future.keywords
|
|
p[x] contains 1 if x = "foo"`,
|
|
expectedErr: "rego_type_error: rule heads with general refs (containing variables) are not supported: p[x]",
|
|
},
|
|
{
|
|
note: "ref-head feature, general-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRefHeads,
|
|
},
|
|
module: `package test
|
|
import future.keywords
|
|
p[x] contains 1 if x = "foo"`,
|
|
},
|
|
{
|
|
note: "rego-v1 feature, general-ref-head with contains kw",
|
|
features: []string{
|
|
FeatureRegoV1,
|
|
},
|
|
module: `package test
|
|
import future.keywords
|
|
p[x] contains 1 if x = "foo"`,
|
|
},
|
|
|
|
{
|
|
note: "no features, rego.v1 import",
|
|
module: `package test
|
|
import rego.v1
|
|
p if { true }`,
|
|
expectedErr: "rego_compile_error: rego.v1 import is not supported",
|
|
},
|
|
{
|
|
note: "rego-v1-import feature, rego.v1 import",
|
|
module: `package test
|
|
import rego.v1
|
|
p if { true }`,
|
|
features: []string{
|
|
FeatureRegoV1Import,
|
|
},
|
|
},
|
|
{
|
|
note: "rego-v1-import feature, rego.v1 import",
|
|
module: `package test
|
|
import rego.v1
|
|
p if { true }`,
|
|
features: []string{
|
|
FeatureRegoV1,
|
|
},
|
|
},
|
|
{
|
|
note: "no features, template-string",
|
|
module: `package test
|
|
p := $"foo {42}"`,
|
|
expectedErr: "rego_compile_error: template-strings are not supported",
|
|
},
|
|
{
|
|
note: "template-string feature, no internal.template_string built-in, template-string",
|
|
module: `package test
|
|
p := $"foo {42}"`,
|
|
features: []string{
|
|
FeatureTemplateStrings,
|
|
},
|
|
builtins: []*Builtin{},
|
|
expectedErr: "rego_compile_error: template-strings are not supported",
|
|
},
|
|
{
|
|
note: "template-string feature, internal.template_string built-in, template-string",
|
|
module: `package test
|
|
p := $"foo {42}"`,
|
|
features: []string{
|
|
FeatureTemplateStrings,
|
|
},
|
|
builtins: []*Builtin{
|
|
InternalTemplateString,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
capabilities := CapabilitiesForThisVersion()
|
|
capabilities.Features = tc.features
|
|
|
|
if tc.builtins != nil {
|
|
capabilities.Builtins = tc.builtins
|
|
}
|
|
|
|
// Modules are parsed with full set of capabilities
|
|
mod := module(tc.module)
|
|
|
|
compiler := NewCompiler().WithCapabilities(capabilities)
|
|
compiler.Compile(map[string]*Module{"test": mod})
|
|
if tc.expectedErr != "" {
|
|
if !compiler.Failed() {
|
|
t.Fatal("expected error but got success")
|
|
}
|
|
if !strings.Contains(compiler.Errors.Error(), tc.expectedErr) {
|
|
t.Fatalf("expected error:\n\n%s\n\nbut got:\n\n%v", tc.expectedErr, compiler.Errors)
|
|
}
|
|
} else if compiler.Failed() {
|
|
t.Fatalf("unexpected error(s): %v", compiler.Errors)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCustomBuiltinWithCompileModulesWithOpt(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
module string
|
|
expectedErrorCode string
|
|
skipCapabilities bool
|
|
}{
|
|
{
|
|
name: "custom builtin",
|
|
module: `package test
|
|
|
|
p if { bar(2) }`,
|
|
},
|
|
{
|
|
name: "missing custom builtin",
|
|
module: `package test
|
|
|
|
p if { foo(1,2,x) }`,
|
|
expectedErrorCode: "rego_type_error",
|
|
},
|
|
{
|
|
name: "no capabilities, using custom builtin",
|
|
module: `package test
|
|
|
|
p if { bar(2) }`,
|
|
skipCapabilities: true,
|
|
expectedErrorCode: "rego_type_error",
|
|
},
|
|
{
|
|
name: "no capabilities",
|
|
module: `package test
|
|
import rego.v1
|
|
p if { true }`,
|
|
skipCapabilities: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
customBuiltin := &Builtin{
|
|
Name: "bar",
|
|
Decl: types.NewFunction([]types.Type{types.N}, types.B),
|
|
}
|
|
|
|
capabilities := CapabilitiesForThisVersion()
|
|
capabilities.Builtins = append(capabilities.Builtins, customBuiltin)
|
|
|
|
var err error
|
|
if tc.skipCapabilities {
|
|
_, err = CompileModulesWithOpt(map[string]string{"x": tc.module}, CompileOpts{})
|
|
} else {
|
|
_, err = CompileModulesWithOpt(map[string]string{"x": tc.module}, CompileOpts{
|
|
ParserOptions: ParserOptions{
|
|
Capabilities: capabilities,
|
|
},
|
|
})
|
|
}
|
|
|
|
if tc.expectedErrorCode == "" && err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.expectedErrorCode != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error code %s but got success", tc.expectedErrorCode)
|
|
}
|
|
var astError Errors
|
|
if errors.As(err, &astError) {
|
|
if astError[0].Code != tc.expectedErrorCode {
|
|
t.Fatalf("expected error code %s but got %s", tc.expectedErrorCode, astError[0].Code)
|
|
}
|
|
} else {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCapabilitiesExtendedWithCustomBuiltins(t *testing.T) {
|
|
|
|
compiler := NewCompiler().WithCapabilities(&Capabilities{
|
|
Builtins: []*Builtin{
|
|
{
|
|
Name: "foo",
|
|
Decl: types.NewFunction([]types.Type{types.N}, types.B),
|
|
},
|
|
},
|
|
}).WithBuiltins(map[string]*Builtin{
|
|
"bar": {
|
|
Name: "bar",
|
|
Decl: types.NewFunction([]types.Type{types.N}, types.B),
|
|
},
|
|
})
|
|
|
|
module1 := module(`package test
|
|
|
|
p if { foo(1); bar(2) }`)
|
|
module2 := module(`package test
|
|
|
|
p if { plus(1,2,x) }`)
|
|
|
|
compiler.Compile(map[string]*Module{"x": module1})
|
|
if compiler.Failed() {
|
|
t.Fatal("unexpected error:", compiler.Errors)
|
|
}
|
|
|
|
compiler.Compile(map[string]*Module{"x": module2})
|
|
if !compiler.Failed() {
|
|
t.Fatal("expected error but got success")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCompilerWithUnsafeBuiltins(t *testing.T) {
|
|
// Rego includes a number of built-in functions. In some cases, you may not
|
|
// want all builtins to be available to a program. This test shows how to
|
|
// mark a built-in as unsafe.
|
|
compiler := NewCompiler().WithUnsafeBuiltins(map[string]struct{}{"re_match": {}})
|
|
|
|
// This query should not compile because the `re_match` built-in is no
|
|
// longer available.
|
|
_, err := compiler.QueryCompiler().Compile(MustParseBody(`re_match("a", "a")`))
|
|
if err == nil {
|
|
t.Fatalf("Expected error for unsafe built-in")
|
|
} else if !strings.Contains(err.Error(), "unsafe built-in function") {
|
|
t.Fatalf("Expected error for unsafe built-in but got %v", err)
|
|
}
|
|
|
|
// These modules should not compile for the same reason.
|
|
modules := map[string]*Module{"mod1": module(`package a.b.c
|
|
deny if {
|
|
re_match(input.user, ".*bob.*")
|
|
}`)}
|
|
compiler.Compile(modules)
|
|
if !compiler.Failed() {
|
|
t.Fatalf("Expected error for unsafe built-in")
|
|
} else if !strings.Contains(compiler.Errors[0].Error(), "unsafe built-in function") {
|
|
t.Fatalf("Expected error for unsafe built-in but got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCompilerPassesTypeCheck(t *testing.T) {
|
|
c := NewCompiler().
|
|
WithCapabilities(&Capabilities{Builtins: []*Builtin{Split}})
|
|
// Must compile to initialize type environment after WithCapabilities
|
|
c.Compile(nil)
|
|
if c.PassesTypeCheck(MustParseBody(`a = input.a; split(a, ":", x); a0 = x[0]; a0 = null`)) {
|
|
t.Fatal("Did not successfully detect a type-checking violation")
|
|
}
|
|
}
|
|
|
|
func TestCompilerPassesTypeCheckRules(t *testing.T) {
|
|
inputSchema := `{
|
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
"description": "OPA Authorization Policy Schema",
|
|
"type": "object",
|
|
"properties": {
|
|
"identity": {
|
|
"type": "string"
|
|
},
|
|
"path": {
|
|
"type": "array",
|
|
"items": {}
|
|
},
|
|
"params": {
|
|
"type": "object"
|
|
}
|
|
},
|
|
"required": [
|
|
"identity",
|
|
"path",
|
|
"params"
|
|
]
|
|
}`
|
|
|
|
ischema := util.MustUnmarshalJSON([]byte(inputSchema))
|
|
|
|
module1 := `
|
|
package policy
|
|
|
|
default allow := false
|
|
|
|
allow if {
|
|
input.identity = "foo"
|
|
}
|
|
|
|
allow if {
|
|
input.path = ["foo", "bar"]
|
|
}
|
|
|
|
allow if {
|
|
input.params = {"foo": "bar"}
|
|
}`
|
|
|
|
module2 := `
|
|
package policy
|
|
|
|
default allow := false
|
|
|
|
allow if {
|
|
input.identty = "foo"
|
|
}`
|
|
|
|
module3 := `
|
|
package policy
|
|
|
|
default allow := false
|
|
|
|
allow if {
|
|
input.path = "foo"
|
|
}`
|
|
|
|
module4 := `
|
|
package policy
|
|
|
|
default allow := false
|
|
|
|
allow if {
|
|
input.identty = "foo"
|
|
}
|
|
|
|
allow if {
|
|
input.path = "foo"
|
|
}`
|
|
|
|
schemaSet := NewSchemaSet()
|
|
schemaSet.Put(SchemaRootRef, ischema)
|
|
|
|
tests := []struct {
|
|
note string
|
|
modules []string
|
|
errs []string
|
|
}{
|
|
{note: "no error", modules: []string{module1}},
|
|
{note: "typo", modules: []string{module2}, errs: []string{"undefined ref: input.identty"}},
|
|
{note: "wrong type", modules: []string{module3}, errs: []string{"match error"}},
|
|
{note: "multiple errors", modules: []string{module4}, errs: []string{"match error", "undefined ref: input.identty"}},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
var elems []*Rule
|
|
|
|
for i, module := range tc.modules {
|
|
mod, err := ParseModuleWithOpts(fmt.Sprintf("test%d.rego", i+1), module, ParserOptions{
|
|
ProcessAnnotation: true,
|
|
AllFutureKeywords: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, rule := range mod.Rules {
|
|
elems = append(elems, rule)
|
|
for next := rule.Else; next != nil; next = next.Else {
|
|
elems = append(elems, next)
|
|
}
|
|
}
|
|
}
|
|
|
|
errs := NewCompiler().WithSchemas(schemaSet).PassesTypeCheckRules(elems)
|
|
|
|
if len(errs) > 0 {
|
|
if len(tc.errs) == 0 {
|
|
t.Fatalf("Unexpected error: %v", errs)
|
|
}
|
|
|
|
result := compilerErrsToStringSlice(errs)
|
|
|
|
if len(result) != len(tc.errs) {
|
|
t.Fatalf("Expected %d:\n%v\nBut got %d:\n%v", len(tc.errs), strings.Join(tc.errs, "\n"), len(result), strings.Join(result, "\n"))
|
|
}
|
|
|
|
for i := range result {
|
|
if !strings.Contains(result[i], tc.errs[i]) {
|
|
t.Errorf("Expected %v but got: %v", tc.errs[i], result[i])
|
|
}
|
|
}
|
|
} else if len(tc.errs) > 0 {
|
|
t.Fatalf("Expected error %q but got success", tc.errs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerPassesTypeCheckNegative(t *testing.T) {
|
|
c := NewCompiler().
|
|
WithCapabilities(&Capabilities{Builtins: []*Builtin{Split, StartsWith}})
|
|
// Must compile to initialize type environment after WithCapabilities
|
|
c.Compile(nil)
|
|
if !c.PassesTypeCheck(MustParseBody(`a = input.a; split(a, ":", x); a0 = x[0]; startswith(a0, "foo", true)`)) {
|
|
t.Fatal("Incorrectly detected a type-checking violation")
|
|
}
|
|
}
|
|
|
|
func TestKeepModules(t *testing.T) {
|
|
|
|
t.Run("no keep", func(t *testing.T) {
|
|
c := NewCompiler() // no keep is default
|
|
|
|
// This one is overwritten by c.Compile()
|
|
c.Modules["foo.rego"] = MustParseModule("package foo\np = true")
|
|
|
|
c.Compile(map[string]*Module{"bar.rego": MustParseModule("package bar\np = input")})
|
|
|
|
if len(c.Errors) != 0 {
|
|
t.Fatalf("expected no error; got %v", c.Errors)
|
|
}
|
|
|
|
mods := c.ParsedModules()
|
|
if mods != nil {
|
|
t.Errorf("expected ParsedModules == nil, got %v", mods)
|
|
}
|
|
})
|
|
|
|
t.Run("keep", func(t *testing.T) {
|
|
|
|
c := NewCompiler().WithKeepModules(true)
|
|
|
|
// This one is overwritten by c.Compile()
|
|
c.Modules["foo.rego"] = MustParseModule("package foo\np = true")
|
|
|
|
c.Compile(map[string]*Module{"bar.rego": MustParseModule("package bar\np = input")})
|
|
if len(c.Errors) != 0 {
|
|
t.Fatalf("expected no error; got %v", c.Errors)
|
|
}
|
|
|
|
mods := c.ParsedModules()
|
|
if exp, act := 1, len(mods); exp != act {
|
|
t.Errorf("expected %d modules, found %d: %v", exp, act, mods)
|
|
}
|
|
for k := range mods {
|
|
if k != "bar.rego" {
|
|
t.Errorf("unexpected key: %v, want 'bar.rego'", k)
|
|
}
|
|
}
|
|
|
|
for k := range mods {
|
|
compiled := c.Modules[k]
|
|
if compiled.Equal(mods[k]) {
|
|
t.Errorf("expected module %v to not be compiled: %v", k, mods[k])
|
|
}
|
|
}
|
|
|
|
// expect ParsedModules to be reset
|
|
c.Compile(map[string]*Module{"baz.rego": MustParseModule("package baz\np = input")})
|
|
mods = c.ParsedModules()
|
|
if exp, act := 1, len(mods); exp != act {
|
|
t.Errorf("expected %d modules, found %d: %v", exp, act, mods)
|
|
}
|
|
for k := range mods {
|
|
if k != "baz.rego" {
|
|
t.Errorf("unexpected key: %v, want 'baz.rego'", k)
|
|
}
|
|
}
|
|
|
|
for k := range mods {
|
|
compiled := c.Modules[k]
|
|
if compiled.Equal(mods[k]) {
|
|
t.Errorf("expected module %v to not be compiled: %v", k, mods[k])
|
|
}
|
|
}
|
|
|
|
// expect ParsedModules to be reset to nil
|
|
c = c.WithKeepModules(false)
|
|
c.Compile(map[string]*Module{"baz.rego": MustParseModule("package baz\np = input")})
|
|
mods = c.ParsedModules()
|
|
if mods != nil {
|
|
t.Errorf("expected ParsedModules == nil, got %v", mods)
|
|
}
|
|
})
|
|
|
|
t.Run("no copies", func(t *testing.T) {
|
|
extra := MustParseModule("package extra\np = input")
|
|
done := false
|
|
testLoader := func(map[string]*Module) (map[string]*Module, error) {
|
|
if done {
|
|
return nil, nil
|
|
}
|
|
done = true
|
|
return map[string]*Module{"extra.rego": extra}, nil
|
|
}
|
|
|
|
c := NewCompiler().WithModuleLoader(testLoader).WithKeepModules(true)
|
|
|
|
mod := MustParseModule("package bar\np = input")
|
|
c.Compile(map[string]*Module{"bar.rego": mod})
|
|
if len(c.Errors) != 0 {
|
|
t.Fatalf("expected no error; got %v", c.Errors)
|
|
}
|
|
|
|
mods := c.ParsedModules()
|
|
if exp, act := 2, len(mods); exp != act {
|
|
t.Errorf("expected %d modules, found %d: %v", exp, act, mods)
|
|
}
|
|
newName := Var("q")
|
|
mods["bar.rego"].Rules[0].Head.Name = newName
|
|
if exp, act := newName, mod.Rules[0].Head.Name; exp != act {
|
|
t.Errorf("expected modified rule name %v, found %v", exp, act)
|
|
}
|
|
mods["extra.rego"].Rules[0].Head.Name = newName
|
|
if exp, act := newName, extra.Rules[0].Head.Name; exp != act {
|
|
t.Errorf("expected modified rule name %v, found %v", exp, act)
|
|
}
|
|
})
|
|
|
|
t.Run("keep, with loader", func(t *testing.T) {
|
|
extra := MustParseModule("package extra\np = input")
|
|
done := false
|
|
testLoader := func(map[string]*Module) (map[string]*Module, error) {
|
|
if done {
|
|
return nil, nil
|
|
}
|
|
done = true
|
|
return map[string]*Module{"extra.rego": extra}, nil
|
|
}
|
|
|
|
c := NewCompiler().WithModuleLoader(testLoader).WithKeepModules(true)
|
|
|
|
// This one is overwritten by c.Compile()
|
|
c.Modules["foo.rego"] = MustParseModule("package foo\np = true")
|
|
|
|
c.Compile(map[string]*Module{"bar.rego": MustParseModule("package bar\np = input")})
|
|
|
|
if len(c.Errors) != 0 {
|
|
t.Fatalf("expected no error; got %v", c.Errors)
|
|
}
|
|
|
|
mods := c.ParsedModules()
|
|
if exp, act := 2, len(mods); exp != act {
|
|
t.Errorf("expected %d modules, found %d: %v", exp, act, mods)
|
|
}
|
|
for k := range mods {
|
|
if k != "bar.rego" && k != "extra.rego" {
|
|
t.Errorf("unexpected key: %v, want 'extra.rego' and 'bar.rego'", k)
|
|
}
|
|
}
|
|
|
|
for k := range mods {
|
|
compiled := c.Modules[k]
|
|
if compiled.Equal(mods[k]) {
|
|
t.Errorf("expected module %v to not be compiled: %v", k, mods[k])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// see https://github.com/open-policy-agent/opa/issues/5166
|
|
func TestCompilerWithRecursiveSchema(t *testing.T) {
|
|
|
|
jsonSchema := `{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://github.com/open-policy-agent/opa/issues/5166",
|
|
"type": "object",
|
|
"properties": {
|
|
"Something": {
|
|
"$ref": "#/$defs/X"
|
|
}
|
|
},
|
|
"$defs": {
|
|
"X": {
|
|
"type": "object",
|
|
"properties": {
|
|
"Name": { "type": "string" },
|
|
"Y": {
|
|
"$ref": "#/$defs/Y"
|
|
}
|
|
}
|
|
},
|
|
"Y": {
|
|
"type": "object",
|
|
"properties": {
|
|
"X": {
|
|
"$ref": "#/$defs/X"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
exampleModule := `# METADATA
|
|
# schemas:
|
|
# - input: schema.input
|
|
package opa.recursion
|
|
|
|
deny if {
|
|
input.Something.Y.X.Name == "Something"
|
|
}
|
|
`
|
|
|
|
c := NewCompiler()
|
|
var schema any
|
|
if err := json.Unmarshal([]byte(jsonSchema), &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schemaSet := NewSchemaSet()
|
|
schemaSet.Put(MustParseRef("schema.input"), schema)
|
|
c.WithSchemas(schemaSet)
|
|
|
|
m := MustParseModuleWithOpts(exampleModule, ParserOptions{
|
|
ProcessAnnotation: true,
|
|
AllFutureKeywords: true,
|
|
})
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
if c.Failed() {
|
|
t.Errorf("Expected compilation to succeed, but got errors: %v", c.Errors)
|
|
}
|
|
}
|
|
|
|
// see https://github.com/open-policy-agent/opa/issues/5166
|
|
func TestCompilerWithRecursiveSchemaAndInvalidSource(t *testing.T) {
|
|
|
|
jsonSchema := `{
|
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
"$id": "https://github.com/open-policy-agent/opa/issues/5166",
|
|
"type": "object",
|
|
"properties": {
|
|
"Something": {
|
|
"$ref": "#/$defs/X"
|
|
}
|
|
},
|
|
"$defs": {
|
|
"X": {
|
|
"type": "object",
|
|
"properties": {
|
|
"Name": { "type": "string" },
|
|
"Y": {
|
|
"$ref": "#/$defs/Y"
|
|
}
|
|
}
|
|
},
|
|
"Y": {
|
|
"type": "object",
|
|
"properties": {
|
|
"X": {
|
|
"$ref": "#/$defs/X"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
exampleModule := `# METADATA
|
|
# schemas:
|
|
# - input: schema.input
|
|
package opa.recursion
|
|
|
|
deny if {
|
|
input.Something.Y.X.ThisDoesNotExist == "Something"
|
|
}
|
|
`
|
|
|
|
c := NewCompiler().
|
|
WithUseTypeCheckAnnotations(true)
|
|
var schema any
|
|
if err := json.Unmarshal([]byte(jsonSchema), &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schemaSet := NewSchemaSet()
|
|
schemaSet.Put(MustParseRef("schema.input"), schema)
|
|
c.WithSchemas(schemaSet)
|
|
|
|
m := MustParseModuleWithOpts(exampleModule, ParserOptions{
|
|
ProcessAnnotation: true,
|
|
AllFutureKeywords: true,
|
|
})
|
|
c.Compile(map[string]*Module{"testMod": m})
|
|
if !c.Failed() {
|
|
t.Errorf("Expected compilation to fail, but it succeeded")
|
|
} else if !strings.HasPrefix(c.Errors.Error(), "1 error occurred: 7:2: rego_type_error: undefined ref: input.Something.Y.X.ThisDoesNotExist") {
|
|
t.Errorf("unexpected error: %v", c.Errors.Error())
|
|
}
|
|
}
|
|
|
|
func modules(ms ...string) []*Module {
|
|
opts := ParserOptions{AllFutureKeywords: true}
|
|
mods := make([]*Module, len(ms))
|
|
for i, m := range ms {
|
|
var err error
|
|
mods[i], err = ParseModuleWithOpts(fmt.Sprintf("mod%d.rego", i), m, opts)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return mods
|
|
}
|
|
|
|
// FIXME(v1-test-refactor): In OPA 1.0, a call to here can be replaced with a call to MustParseModule.
|
|
func module(raw string, opts ...func(ParserOptions) ParserOptions) *Module {
|
|
popts := ParserOptions{}
|
|
|
|
for _, opt := range opts {
|
|
popts = opt(popts)
|
|
}
|
|
|
|
lessRaw := strings.TrimSpace(raw)
|
|
if !strings.HasPrefix(lessRaw, "package ") && !strings.HasPrefix(lessRaw, "#") {
|
|
raw = "package test\n\n" + raw
|
|
}
|
|
|
|
return MustParseModuleWithOpts(raw, popts)
|
|
}
|
|
|
|
func TestCompilerWithRecursiveSchemaAvoidRace(t *testing.T) {
|
|
|
|
jsonSchema := `{
|
|
"type": "object",
|
|
"properties": {
|
|
"aws": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.AWS"
|
|
}
|
|
},
|
|
"$defs": {
|
|
"example.pkg.providers.aws.AWS": {
|
|
"type": "object",
|
|
"properties": {
|
|
"iam": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.iam.IAM"
|
|
},
|
|
"sqs": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.sqs.SQS"
|
|
}
|
|
}
|
|
},
|
|
"example.pkg.providers.aws.iam.Document": {
|
|
"type": "object"
|
|
},
|
|
"example.pkg.providers.aws.iam.IAM": {
|
|
"type": "object",
|
|
"properties": {
|
|
"policies": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.iam.Policy"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"example.pkg.providers.aws.iam.Policy": {
|
|
"type": "object",
|
|
"properties": {
|
|
"builtin": {
|
|
"type": "object",
|
|
"properties": {
|
|
"value": {
|
|
"type": "boolean"
|
|
}
|
|
}
|
|
},
|
|
"document": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.iam.Document"
|
|
}
|
|
}
|
|
},
|
|
"example.pkg.providers.aws.sqs.Queue": {
|
|
"type": "object",
|
|
"properties": {
|
|
"policies": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.iam.Policy"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"example.pkg.providers.aws.sqs.SQS": {
|
|
"type": "object",
|
|
"properties": {
|
|
"queues": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"$ref": "#/$defs/example.pkg.providers.aws.sqs.Queue"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
|
|
exampleModule := `# METADATA
|
|
# schemas:
|
|
# - input: schema.input
|
|
package race.condition
|
|
|
|
deny if {
|
|
queue := input.aws.sqs.queues[_]
|
|
policy := queue.policies[_]
|
|
doc := json.unmarshal(policy.document.value)
|
|
statement = doc.Statement[_]
|
|
action := statement.Action[_]
|
|
action == "*"
|
|
}
|
|
`
|
|
|
|
var schema any
|
|
if err := json.Unmarshal([]byte(jsonSchema), &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schemaSet := NewSchemaSet()
|
|
schemaSet.Put(MustParseRef("schema.input"), schema)
|
|
|
|
c := NewCompiler().WithSchemas(schemaSet)
|
|
c.Compile(map[string]*Module{"testMod": MustParseModuleWithOpts(exampleModule, ParserOptions{
|
|
ProcessAnnotation: true,
|
|
AllFutureKeywords: true,
|
|
})})
|
|
assertNotFailed(t, c)
|
|
}
|
|
|
|
func TestCompilerRewriteTestRulesForTracing(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
rewrite bool
|
|
module string
|
|
exp string
|
|
}{
|
|
{
|
|
note: "ref comparison, no rewrite",
|
|
module: `package test
|
|
|
|
a := 1
|
|
b := 2
|
|
|
|
test_something if {
|
|
a == b
|
|
}`,
|
|
exp: `package test
|
|
|
|
a := 1 if { true }
|
|
b := 2 if { true }
|
|
|
|
test_something = true if {
|
|
data.test.a = data.test.b
|
|
}`,
|
|
},
|
|
{
|
|
note: "ref comparison, rewrite",
|
|
rewrite: true,
|
|
module: `package test
|
|
|
|
a := 1
|
|
b := 2
|
|
|
|
test_something if {
|
|
a == b
|
|
}`,
|
|
// When the test fails on '__local0__ = __local1__', the values for 'a' and 'b' are captured in local bindings,
|
|
// accessible by the tracer.
|
|
exp: `package test
|
|
|
|
a := 1 if { true }
|
|
b := 2 if { true }
|
|
|
|
test_something = true if {
|
|
__local0__ = data.test.a
|
|
__local1__ = data.test.b
|
|
__local0__ = __local1__
|
|
}`,
|
|
},
|
|
{
|
|
note: "ref comparison, not-stmt, rewrite",
|
|
rewrite: true,
|
|
module: `package test
|
|
|
|
a := 1
|
|
b := 2
|
|
|
|
test_something if {
|
|
not a == b
|
|
}`,
|
|
// We don't break out local vars from a not-stmt, as that would change the semantics of the rule.
|
|
exp: `package test
|
|
|
|
a := 1 if { true }
|
|
b := 2 if { true }
|
|
|
|
test_something = true if {
|
|
not data.test.a = data.test.b
|
|
}`,
|
|
},
|
|
{
|
|
note: "ref comparison, inside every-stmt, no rewrite",
|
|
module: `package test
|
|
|
|
a := 1
|
|
b := 2
|
|
l := [1, 2, 3]
|
|
|
|
test_something if {
|
|
every x in l {
|
|
a < b + x
|
|
}
|
|
}`,
|
|
exp: `package test
|
|
|
|
a := 1 if { true }
|
|
b := 2 if { true }
|
|
l := [1, 2, 3] if { true }
|
|
|
|
test_something = true if {
|
|
__local2__ = data.test.l
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local4__ = data.test.b
|
|
plus(__local4__, __local1__, __local3__)
|
|
__local5__ = data.test.a
|
|
lt(__local5__, __local3__)
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
note: "ref comparison, inside every-stmt, rewrite",
|
|
rewrite: true,
|
|
module: `package test
|
|
|
|
a := 1
|
|
b := 2
|
|
l := [1, 2, 3]
|
|
|
|
test_something if {
|
|
every x in l {
|
|
a < b + x
|
|
}
|
|
}`,
|
|
// When tests contain an 'every' statement, we're interested in the circumstances that made the every fail,
|
|
// so it's body is rewritten.
|
|
exp: `package test
|
|
|
|
a := 1 if { true }
|
|
b := 2 if { true }
|
|
l := [1, 2, 3] if { true }
|
|
|
|
test_something = true if {
|
|
__local2__ = data.test.l;
|
|
every __local0__, __local1__ in __local2__ {
|
|
__local4__ = data.test.b
|
|
plus(__local4__, __local1__, __local3__)
|
|
__local5__ = data.test.a
|
|
lt(__local5__, __local3__)
|
|
}
|
|
}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
ms := map[string]string{
|
|
"test.rego": tc.module,
|
|
}
|
|
c := getCompilerWithParsedModules(ms).
|
|
WithRewriteTestRules(tc.rewrite)
|
|
|
|
compileStages(c, StageRewriteTestRulesForTracing)
|
|
assertNotFailed(t, c)
|
|
|
|
result := c.Modules["test.rego"]
|
|
exp := module(tc.exp)
|
|
exp.Imports = nil // We strip the imports since the compiler will too
|
|
if result.Compare(exp) != 0 {
|
|
t.Fatalf("\nExpected:\n\n%v\n\nGot:\n\n%v", exp, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompile_DefaultRegoVersion(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
modules map[string]*Module
|
|
expErrs Errors
|
|
}{
|
|
{
|
|
note: "no module rego-version, no v1 violations",
|
|
modules: map[string]*Module{
|
|
"test": {
|
|
Package: MustParsePackage(`package test`),
|
|
Imports: MustParseImports(`import data.foo
|
|
import data.bar`),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "no module rego-version, v1 violations", // default is v1, errors expected
|
|
modules: map[string]*Module{
|
|
"test": {
|
|
Package: MustParsePackage(`package test`),
|
|
Imports: MustParseImports(`import data.foo
|
|
import data.bar as foo`),
|
|
},
|
|
},
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "import must not shadow import data.foo",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "v0 module, v1 violations",
|
|
modules: map[string]*Module{
|
|
"test": MustParseModuleWithOpts(`package test
|
|
import data.foo
|
|
import data.bar as foo`,
|
|
ParserOptions{RegoVersion: RegoV0}),
|
|
},
|
|
},
|
|
{
|
|
note: "v1 module, v1 violations",
|
|
modules: map[string]*Module{
|
|
"test": MustParseModuleWithOpts(`package test
|
|
import data.foo
|
|
import data.bar as foo`,
|
|
ParserOptions{RegoVersion: RegoV1}),
|
|
},
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "import must not shadow import data.foo",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
compiler := NewCompiler()
|
|
|
|
compiler.Compile(tc.modules)
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
assertErrors(t, compiler.Errors, tc.expErrs, false)
|
|
} else if len(compiler.Errors) > 0 {
|
|
t.Fatalf("Unexpected errors: %v", compiler.Errors)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerInitWithDefaultModuleLoader(t *testing.T) {
|
|
// Reset the global variable after the test
|
|
defer func() { defaultModuleLoader = nil }()
|
|
|
|
// a dummy loader that adds "foo"
|
|
loader1 := func(res map[string]*Module) (map[string]*Module, error) {
|
|
mod := MustParseModule(`package foo`)
|
|
resCopy := map[string]*Module{}
|
|
maps.Copy(resCopy, res)
|
|
resCopy["foo.rego"] = mod
|
|
return resCopy, nil
|
|
}
|
|
|
|
// a dummy loader that adds "bar"
|
|
loader2 := func(res map[string]*Module) (map[string]*Module, error) {
|
|
mod := MustParseModule(`package bar`)
|
|
resCopy := map[string]*Module{}
|
|
maps.Copy(resCopy, res)
|
|
resCopy["bar.rego"] = mod
|
|
return resCopy, nil
|
|
}
|
|
|
|
DefaultModuleLoader(loader2)
|
|
|
|
c := NewCompiler().WithModuleLoader(loader1)
|
|
c.init()
|
|
|
|
got, err := c.moduleLoader(make(map[string]*Module))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
expected := map[string]*Module{
|
|
"foo.rego": MustParseModule(`package foo`),
|
|
"bar.rego": MustParseModule(`package bar`),
|
|
}
|
|
// check both modules are present
|
|
for k, v := range expected {
|
|
gotMod, ok := got[k]
|
|
if !ok {
|
|
t.Errorf("expected key %q in result", k)
|
|
continue
|
|
}
|
|
if !reflect.DeepEqual(gotMod, v) {
|
|
t.Errorf("unexpected module for %q: got %v want %v", k, gotMod, v)
|
|
}
|
|
}
|
|
|
|
// Now, test defaultModuleLoader only
|
|
c2 := NewCompiler()
|
|
c2.init()
|
|
got2, err := c2.moduleLoader(make(map[string]*Module))
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if _, ok := got2["bar.rego"]; !ok {
|
|
t.Error("expected bar.rego from defaultModuleLoader in result")
|
|
}
|
|
}
|
|
|
|
// Verify fix for https://github.com/open-policy-agent/opa/issues/8158
|
|
func TestCompilerCopiesTemplateStrings(t *testing.T) {
|
|
mod := MustParseModule(`package p
|
|
s contains z if {
|
|
some y in [1, 2, 3]
|
|
z := $"{y} "
|
|
}`)
|
|
cpy := mod.Copy()
|
|
|
|
c1 := NewCompiler()
|
|
if c1.Compile(map[string]*Module{"p.rego": mod}); c1.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c1.Errors)
|
|
}
|
|
|
|
c2 := NewCompiler()
|
|
if c2.Compile(map[string]*Module{"p.rego": mod}); c2.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c2.Errors)
|
|
}
|
|
|
|
if !mod.Equal(cpy) {
|
|
t.Fatalf("expected module to be unchanged after compilation")
|
|
}
|
|
}
|
|
|
|
func TestCompilerNotImport(t *testing.T) {
|
|
popts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(),
|
|
FutureKeywords: []string{"not"},
|
|
}
|
|
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expMod *Module
|
|
expErrs Errors
|
|
}{
|
|
{
|
|
note: "no import",
|
|
module: `package negation
|
|
p if {
|
|
not input.x + input.y == input.z
|
|
}
|
|
`,
|
|
expMod: MustParseModule(`package negation
|
|
p = true if {
|
|
__local1__ = input.x
|
|
__local2__ = input.y
|
|
plus(__local1__, __local2__, __local0__)
|
|
not __local0__ = input.z
|
|
}
|
|
`),
|
|
},
|
|
{
|
|
note: "negated call, equal",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
p if {
|
|
not 1 + 2 == 3
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated call, unification",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
p if {
|
|
not 1 + 2 = 3
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated call, unification, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
p if {
|
|
not { 1 + 2 = 3 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated call with refs (local var expansion)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
p if {
|
|
not input.x + input.y == input.z
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
__local1__ = input.x
|
|
__local2__ = input.y
|
|
plus(__local1__, __local2__, __local0__)
|
|
__local0__ = input.z
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated call with vars, inside comprehension",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p := [x | x := "foo"; not f(input.x)]
|
|
|
|
f(_) := true
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p := __local2__ if {
|
|
__local2__ = [__local0__ | __local0__ = "foo"
|
|
not {
|
|
__local3__ = input.x
|
|
data.negation.f(__local3__)
|
|
}
|
|
]
|
|
}
|
|
|
|
f(__local1__) := true if { true }
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated call with vars, inside comprehension, unsafe assignment",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p := [x | not x := "foo" ]
|
|
|
|
f(_) := true
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 4, Col: 15, Text: []byte(`not x := "foo"`)},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated call with vars, inside every",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
every x in input.x {
|
|
not f(x, input.y)
|
|
}
|
|
}
|
|
|
|
f(_, _) := true
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
__local4__ = input.x
|
|
every __local0__, __local1__ in __local4__ {
|
|
not {
|
|
__local5__ = input.y
|
|
data.negation.f(__local1__, __local5__)
|
|
}
|
|
}
|
|
}
|
|
|
|
f(__local2__, __local3__) := true if { true }
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated assignment",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a := 1
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe", // FIXME: Use more specific error msg: "cannot assign vars inside negated expression"
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a := 1")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated assignment, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a := 1 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not { __local0__ = 1 }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated assignment, call",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a := 1 + 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a := 1 + 2")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated assignment, call, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a := 1 + 2 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, __local1__)
|
|
__local0__ = __local1__
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated assignment, call, output var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not plus(1, 2, a)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not plus(1, 2, a)")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated assignment, call, output var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { plus(1, 2, a) }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, a)
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated equality, unsafe var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a == 1
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a == 1")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated equality, unsafe var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a == 1 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 12, Text: []byte("a == 1")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated unification, unsafe var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a = 1
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a = 1")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated unification, unsafe var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a = 1 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {a = 1}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated unification, unsafe var, call",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a = 1 + 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a = 1 + 2")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated unification, call, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a = 1 + 2 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
plus(1, 2, __local0__)
|
|
a = __local0__
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated enumeration, unsafe var (wildcard)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not input.a[_]
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var _ is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not input.a[_]")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated enumeration, wildcard, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { input.a[_] }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not { input.a[_] }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated safe var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 1
|
|
not a + 2 = 3
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
plus(a, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated safe var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 1
|
|
not { a + 2 = 3 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
plus(a, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated safe var, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a + 2 = 3
|
|
a = 1
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
plus(a, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated safe var, explicit not-body, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a + 2 = 3 }
|
|
a = 1
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
plus(a, 2, __local0__)
|
|
__local0__ = 3
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated unsafe var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a + 2 = 3
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 6, Text: []byte("not a + 2 = 3")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "negated unsafe var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a + 2 = 3 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 12, Text: []byte("a + 2")},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "negated and non-negated unification on same var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 2
|
|
not a = 1
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 2
|
|
not { a = 1 }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated and non-negated unification on same var, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 2
|
|
not { a = 1 }
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 2
|
|
not { a = 1 }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated assignment override, explicit not-body",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 2
|
|
not {
|
|
a := 1
|
|
a == 3
|
|
}
|
|
a == 2
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 2
|
|
not {
|
|
__local0__ = 1
|
|
__local0__ = 3
|
|
}
|
|
a = 2
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated and non-negated unification on same var, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a = 1
|
|
a = 2
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 2
|
|
not { a = 1 }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated and non-negated unification on same var, explicit not-body, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a = 1 }
|
|
a = 2
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 2
|
|
not { a = 1 }
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated and non-negated unification on same var, calls, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not a = f(1)
|
|
a = f(2)
|
|
}
|
|
|
|
f(x) := x
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
data.negation.f(2, __local2__)
|
|
a = __local2__
|
|
not {
|
|
data.negation.f(1, __local1__)
|
|
a = __local1__
|
|
}
|
|
}
|
|
|
|
f(__local0__) := __local0__ if { true }
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "negated and non-negated unification on same var, calls, explicit not-body, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { a = f(1) }
|
|
a = f(2)
|
|
}
|
|
|
|
f(x) := x
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
data.negation.f(2, __local2__)
|
|
a = __local2__
|
|
not {
|
|
data.negation.f(1, __local1__)
|
|
a = __local1__
|
|
}
|
|
}
|
|
|
|
f(__local0__) := __local0__ if { true }
|
|
`, popts),
|
|
},
|
|
|
|
{
|
|
note: "nested negation (comprehension)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not [v |
|
|
v := 1
|
|
not v = 2
|
|
]
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
[__local0__ |
|
|
__local0__ = 1
|
|
not { __local0__ = 2 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), explicit not-bodies",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
[v |
|
|
v := 1
|
|
not { v = 2 }
|
|
]
|
|
}
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
[__local0__ |
|
|
__local0__ = 1
|
|
not { __local0__ = 2 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), outer closure var reference",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
v := 1
|
|
not [42 |
|
|
not v = 2
|
|
]
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
__local0__ = 1
|
|
not {
|
|
[42 |
|
|
not { __local0__ = 2 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), outer closure var reference, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not [42 |
|
|
not v = 2
|
|
]
|
|
v = 1
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
v = 1
|
|
not {
|
|
[42 |
|
|
not { v = 2 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), outer closure var reference, explicit not-bodies, rearranged",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
[42 |
|
|
not { v = 2 }
|
|
]
|
|
}
|
|
v = 1
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
v = 1
|
|
not {
|
|
[42 |
|
|
not { v = 2 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), unsafe var reference",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not [42 |
|
|
not v = 2
|
|
]
|
|
}`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var v is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 6, Col: 7, Text: []byte("not v = 2")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "nested negation (comprehension), inner var override",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
v := 1
|
|
not [42 |
|
|
v := 2
|
|
not v = 1
|
|
]
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
__local0__ = 1
|
|
not {
|
|
[42 |
|
|
__local1__ = 2
|
|
not { __local1__ = 1 }
|
|
]
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
|
|
// Explicit not-bodies required
|
|
|
|
{
|
|
note: "explicit not-body, enumeration",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
x = [1, 2][_]
|
|
x > 2
|
|
}
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p if {
|
|
not {
|
|
__local0__ = [1, 2]
|
|
x = __local0__[_]
|
|
gt(x, 2)
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, enumeration, reordering",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
x = [1, 2][_]
|
|
x > y
|
|
}
|
|
y = 2
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p if {
|
|
y = 2
|
|
not {
|
|
__local0__ = [1, 2]
|
|
x = __local0__[_]
|
|
gt(x, y)
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
|
|
{
|
|
note: "explicit not-body, enumeration, some in",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
some x in [1, 2]
|
|
x > 3
|
|
}
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
__local3__ = [1, 2]
|
|
__local2__ = __local3__[__local1__]
|
|
gt(__local2__, 3)
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, enumeration, some in, reordering",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
some x in [1, 2]
|
|
x < y
|
|
}
|
|
y = 3
|
|
}`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
y = 3
|
|
not {
|
|
__local3__ = [1, 2]
|
|
__local2__ = __local3__[__local1__]
|
|
lt(__local2__, y)
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
|
|
{
|
|
note: "explicit not-body, var indirection",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
a = 1
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
not {
|
|
a = 1
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, var indirection, safe var (unification)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
x = 1
|
|
not {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
x = 1
|
|
not {
|
|
a = x
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, var indirection, safe var (assignment)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
x := 1
|
|
not {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
__local0__ = 1
|
|
not {
|
|
a = __local0__
|
|
b = a
|
|
plus(a, 1, __local1__)
|
|
__local1__ = 2
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, var indirection, unsafe var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 6, Col: 7, Text: []byte("a = x")},
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 6, Col: 7, Text: []byte("a = x")},
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var b is unsafe",
|
|
Location: &Location{File: "mod.rego", Row: 7, Col: 7, Text: []byte("b = a")},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "explicit not-body, nested negations",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if not {
|
|
not {
|
|
not false
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p if not {
|
|
not {
|
|
not false
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, nested negations, ref to outer var",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 1
|
|
not {
|
|
a = 2
|
|
not {
|
|
a = 3
|
|
not a = 4
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p if {
|
|
a = 1
|
|
not {
|
|
a = 2
|
|
not {
|
|
a = 3
|
|
not a = 4
|
|
}
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, nested negations, ref to outer var, inner override",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 1
|
|
not {
|
|
a := 2
|
|
not {
|
|
a = 3
|
|
not a = 4
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
__local0__ = 2
|
|
not {
|
|
__local0__ = 3
|
|
not { __local0__ = 4 }
|
|
}
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
{
|
|
note: "explicit not-body, nested negations, ref to outer var, multiple inner overrides",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
a = 1
|
|
not {
|
|
a := 2
|
|
not {
|
|
a := 3
|
|
not a = 4
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package negation
|
|
p = true if {
|
|
a = 1
|
|
not {
|
|
__local0__ = 2
|
|
not {
|
|
__local1__ = 3
|
|
not { __local1__ = 4 }
|
|
}
|
|
}
|
|
}
|
|
`, popts),
|
|
},
|
|
|
|
{
|
|
note: "no import, negated undefined function (regression: GH#8717)",
|
|
module: `package negation
|
|
|
|
p if {
|
|
not f(1)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: TypeErr,
|
|
Message: "undefined function f",
|
|
Location: &Location{File: "mod.rego", Row: 4, Col: 7, Text: []byte("not f(1)")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "implicit not-body, negated undefined function (regression: GH#8717)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not f(1)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: TypeErr,
|
|
Message: "undefined function f",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 7, Text: []byte("not f(1)")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "explicit not-body, negated undefined function (regression: GH#8717)",
|
|
module: `package negation
|
|
import future.keywords.not
|
|
|
|
p if {
|
|
not { f(1) }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: TypeErr,
|
|
Message: "undefined function f",
|
|
Location: &Location{File: "mod.rego", Row: 5, Col: 13, Text: []byte("f(1)")},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
mod, err := ParseModuleWithOpts("mod.rego", tc.module, ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{"mod.rego": mod})
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
assertErrors(t, c.Errors, tc.expErrs, true)
|
|
} else if len(c.Errors) > 0 {
|
|
if c.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c.Errors)
|
|
}
|
|
}
|
|
|
|
if tc.expMod != nil {
|
|
if diff := cmp.Diff(tc.expMod, c.Modules["mod.rego"]); diff != "" {
|
|
t.Errorf("unexpected module (-want, +got):\n%s", diff)
|
|
}
|
|
|
|
// Regression guard for the future.keywords.not location bug (GH#8717)
|
|
WalkExprs(c.Modules["mod.rego"], func(expr *Expr) bool {
|
|
not, ok := expr.Terms.(*Not)
|
|
if !ok {
|
|
return false
|
|
}
|
|
for i, inner := range not.Body {
|
|
if inner.Location == nil {
|
|
t.Errorf("Not.Body[%d] missing Location: %v", i, inner)
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
for n, m := range c.Modules {
|
|
t.Logf("compiled module %s:\n\n%v\n\n%s", n, m, mermaidGraph(m))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerAndOrRegoVersionParity(t *testing.T) {
|
|
tests := []struct {
|
|
note string
|
|
regoVersion RegoVersion
|
|
module string
|
|
}{
|
|
{
|
|
note: "v0",
|
|
regoVersion: RegoV0,
|
|
module: `package logic
|
|
p {
|
|
x := 1
|
|
y := 2
|
|
z := 3
|
|
x and y or z
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "v1",
|
|
regoVersion: RegoV1,
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
y := 2
|
|
z := 3
|
|
x and y or z
|
|
}
|
|
`,
|
|
},
|
|
}
|
|
|
|
const expectedBody = `__local0__ = 1; __local1__ = 2; __local2__ = 3; __local0__ and __local1__ or __local2__`
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
popts := ParserOptions{
|
|
RegoVersion: tc.regoVersion,
|
|
Capabilities: CapabilitiesForThisVersion(
|
|
CapabilitiesRegoVersion(tc.regoVersion),
|
|
CapabilitiesExperimentalKeywords(true),
|
|
),
|
|
FutureKeywords: []string{"and", "or"},
|
|
}
|
|
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{
|
|
"mod.rego": MustParseModuleWithOpts(tc.module, popts),
|
|
})
|
|
if c.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c.Errors)
|
|
}
|
|
|
|
if got := c.Modules["mod.rego"].Rules[0].Body.String(); got != expectedBody {
|
|
t.Fatalf("compiled body diverged from cross-version baseline:\n want: %s\n got: %s", expectedBody, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerAndOrImports(t *testing.T) {
|
|
popts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or"},
|
|
}
|
|
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expMod any
|
|
expErrs Errors
|
|
}{
|
|
{
|
|
note: "and, or, simple",
|
|
module: `package logic
|
|
|
|
p if {
|
|
x := 1
|
|
y := 2
|
|
z := 3
|
|
x and y or z
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local1__ = 2
|
|
__local2__ = 3
|
|
__local0__ and __local1__ or __local2__
|
|
}`,
|
|
},
|
|
{
|
|
note: "and, assignment, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
x := 1 and y := 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, assignment, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
x := 1 or y := 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, unification, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
x = 1 and y = 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, unification, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
x = 1 or y = 2
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, call output, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
count(input.x, x) and count(input.y, y)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, call output, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
count(input.x, x) or count(input.y, y)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, ref var unification, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
input[x] and input[y]
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, ref var unification, forbidden in implicit body",
|
|
module: `package logic
|
|
p if {
|
|
input[x] or input[y]
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, explicit body, assignment",
|
|
module: `package logic
|
|
p if {
|
|
true and {
|
|
x := 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true and {
|
|
__local0__ = 2
|
|
gt(__local0__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, explicit body, assignment",
|
|
module: `package logic
|
|
p if {
|
|
true or {
|
|
x := 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true or {
|
|
__local0__ = 2
|
|
gt(__local0__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, explicit body, assignment, local bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and {
|
|
x := 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and {
|
|
__local1__ = 2
|
|
gt(__local1__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, explicit body, assignment, local bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or {
|
|
x := 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or {
|
|
__local1__ = 2
|
|
gt(__local1__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, explicit body, unification, no local bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and {
|
|
x = 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and {
|
|
__local0__ = 2
|
|
gt(__local0__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, explicit body, unification, no local bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or {
|
|
x = 2
|
|
x > 1
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or {
|
|
__local0__ = 2
|
|
gt(__local0__, 1)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, local assignment not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ x := 1 } and { y := 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, local assignment not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ x := 1 } or { y := 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, local unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ x = 1 } and { y = 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, local unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ x = 1 } or { y = 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, local ref unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ input[x] = 1 } and { input[y] = 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, local ref unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ input[x] = 1 } or { input[y] = 2 }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, local call unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ count(input.x, x) } and { count(input.y, y) }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, local call unification not visible in outer scope",
|
|
module: `package logic
|
|
p := [x, y] if {
|
|
{ count(input.x, x) } or { count(input.y, y) }
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "and, implicit body, expression expansion",
|
|
module: `package logic
|
|
p if {
|
|
input.x > 1 and input.y < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{
|
|
__local0__ = input.x
|
|
gt(__local0__, 1)
|
|
} and {
|
|
__local1__ = input.y
|
|
lt(__local1__, 2)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, implicit body, expression expansion",
|
|
module: `package logic
|
|
p if {
|
|
input.x > 1 or input.y < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{
|
|
__local0__ = input.x
|
|
gt(__local0__, 1)
|
|
} or {
|
|
__local1__ = input.y
|
|
lt(__local1__, 2)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, implicit body, nested call expansion (multi-expr at safety time)",
|
|
module: `package logic
|
|
p if {
|
|
(input.x + 1) > 0 and input.y > 0
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{
|
|
__local1__ = input.x
|
|
plus(__local1__, 1, __local0__)
|
|
gt(__local0__, 0)
|
|
} and {
|
|
__local2__ = input.y
|
|
gt(__local2__, 0)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, implicit body, nested call expansion (multi-expr at safety time)",
|
|
module: `package logic
|
|
p if {
|
|
input.x > 0 or (input.y * 2) > 0
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{
|
|
__local1__ = input.x
|
|
gt(__local1__, 0)
|
|
} or {
|
|
__local2__ = input.y
|
|
mul(__local2__, 2, __local0__)
|
|
gt(__local0__, 0)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, implicit body, nested call with unbound user vars",
|
|
module: `package logic
|
|
p if {
|
|
(x + 1) > 0 and y > 0
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
note: "and, explicit body, var indirection",
|
|
module: `package logic
|
|
p if {
|
|
true and {
|
|
a = 1
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true and {
|
|
a = 1
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, explicit body, var indirection",
|
|
module: `package logic
|
|
p if {
|
|
true or {
|
|
a = 1
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true or {
|
|
a = 1
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, explicit body, var indirection, unsafe",
|
|
module: `package logic
|
|
p if {
|
|
true and {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var b is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "or, explicit body, var indirection, unsafe",
|
|
module: `package logic
|
|
p if {
|
|
true or {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var a is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var b is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "and, explicit body, var indirection, outer safe ref",
|
|
module: `package logic
|
|
p if {
|
|
x = 1
|
|
true and {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
x = 1
|
|
true and {
|
|
a = x
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, explicit body, var indirection, outer safe ref",
|
|
module: `package logic
|
|
p if {
|
|
x = 1
|
|
true or {
|
|
a = x
|
|
b = a
|
|
a + 1 = 2
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
x = 1
|
|
true or {
|
|
a = x
|
|
b = a
|
|
plus(a, 1, __local0__)
|
|
__local0__ = 2
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, nested, implicit bodies",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and true and x
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and true and __local0__
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, nested, implicit bodies",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or true or x
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or true or __local0__
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, nested, explicit bodies",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and { true and x }
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and { true and __local0__ }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, nested, explicit bodies",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or { true or x }
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or { true or __local0__ }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, nested, explicit bodies, inner bind",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and {
|
|
y := 2
|
|
x and y
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and {
|
|
__local1__ = 2
|
|
__local0__ and __local1__
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, nested, explicit bodies, inner bind",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or {
|
|
y := 2
|
|
x or y
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or {
|
|
__local1__ = 2
|
|
__local0__ or __local1__
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, nested, explicit bodies, inner bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x and {
|
|
x := 2
|
|
y := 3
|
|
x and y
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ and {
|
|
__local1__ = 2
|
|
__local2__ = 3
|
|
__local1__ and __local2__
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, nested, explicit bodies, inner bind override",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x or {
|
|
x := 2
|
|
y := 3
|
|
x or y
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local0__ or {
|
|
__local1__ = 2
|
|
__local2__ = 3
|
|
__local1__ or __local2__
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, with",
|
|
module: `package logic
|
|
p if {
|
|
true and f(42) with input.x as 1
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true and data.logic.f(42) with input.x as 1
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, with",
|
|
module: `package logic
|
|
p if {
|
|
true or f(42) with input.x as 1
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true or data.logic.f(42) with input.x as 1
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, with, inner LHS",
|
|
module: `package logic
|
|
p if {
|
|
{ f(42) with input.x as 1 } and true
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{ data.logic.f(42) with input.x as 1 } and true
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, with, inner LHS",
|
|
module: `package logic
|
|
p if {
|
|
{ f(42) with input.x as 1 } or true
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
{ data.logic.f(42) with input.x as 1 } or true
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, with, inner RHS",
|
|
module: `package logic
|
|
p if {
|
|
true and { f(42) with input.x as 1 }
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true and { data.logic.f(42) with input.x as 1 }
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, with, inner RHS",
|
|
module: `package logic
|
|
p if {
|
|
true or { f(42) with input.x as 1 }
|
|
}
|
|
|
|
f(x) := x + input.x
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
true or { data.logic.f(42) with input.x as 1 }
|
|
}
|
|
|
|
f(__local0__) := __local1__ if {
|
|
__local2__ = input.x
|
|
plus(__local0__, __local2__, __local1__)
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, outer negation",
|
|
module: `package logic
|
|
import future.keywords.not
|
|
p if {
|
|
x := 1
|
|
not {
|
|
y := 2
|
|
x != y and x < y
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
not {
|
|
__local1__ = 2
|
|
neq(__local0__, __local1__) and lt(__local0__, __local1__)
|
|
}
|
|
}
|
|
`, ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or", "not"},
|
|
}),
|
|
},
|
|
{
|
|
note: "or, outer negation",
|
|
module: `package logic
|
|
import future.keywords.not
|
|
p if {
|
|
x := 1
|
|
not {
|
|
y := 2
|
|
x != y or x < y
|
|
}
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
not {
|
|
__local1__ = 2
|
|
neq(__local0__, __local1__) or lt(__local0__, __local1__)
|
|
}
|
|
}
|
|
`, ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or", "not"},
|
|
}),
|
|
},
|
|
{
|
|
note: "and, LHS negation",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
not x != 2 and x < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
{ not neq(__local0__, 2) } and lt(__local0__, 2)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, LHS negation",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
not x != 2 or x < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
{ not neq(__local0__, 2) } or lt(__local0__, 2)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "and, RHS negation",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x != 2 and not x < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
neq(__local0__, 2) and { not lt(__local0__, 2) }
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, RHS negation",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
x != 2 or not x < 2
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
neq(__local0__, 2) or { not lt(__local0__, 2) }
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, every",
|
|
module: `package logic
|
|
p if {
|
|
every x in [1, 2, 3] {
|
|
x > 0 and x < 4
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local2__ = [1, 2, 3]
|
|
every __local0__, __local1__ in __local2__ {
|
|
gt(__local1__, 0) and lt(__local1__, 4)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, every",
|
|
module: `package logic
|
|
p if {
|
|
every x in [1, 2, 3] {
|
|
x > 0 or x < 4
|
|
}
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local2__ = [1, 2, 3]
|
|
every __local0__, __local1__ in __local2__ {
|
|
gt(__local1__, 0) or lt(__local1__, 4)
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
|
|
{
|
|
note: "and, comprehension",
|
|
module: `package logic
|
|
p := [x | some x in input.x; x > 0 and x < 10]
|
|
`,
|
|
expMod: `package logic
|
|
p := __local3__ if {
|
|
__local3__ = [__local2__ |
|
|
__local2__ = input.x[__local1__]
|
|
gt(__local2__, 0) and lt(__local2__, 10)
|
|
]
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "or, comprehension",
|
|
module: `package logic
|
|
p := [x | some x in input.x; x > 0 or x < 10]
|
|
`,
|
|
expMod: `package logic
|
|
p := __local3__ if {
|
|
__local3__ = [__local2__ |
|
|
__local2__ = input.x[__local1__]
|
|
gt(__local2__, 0) or lt(__local2__, 10)
|
|
]
|
|
}
|
|
`,
|
|
},
|
|
|
|
// Parenthesized grouping
|
|
{
|
|
note: "group rhs, safe refs",
|
|
module: `package logic
|
|
p if {
|
|
input.a and (input.b or input.c)
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
input.a and (input.b or input.c)
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "group lhs, safe refs",
|
|
module: `package logic
|
|
p if {
|
|
(input.a or input.b) and input.c
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
(input.a or input.b) and input.c
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "nested groups, redundant outer parens dropped",
|
|
module: `package logic
|
|
p if {
|
|
(input.a and (input.b or input.c)) and input.d
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
input.a and (input.b or input.c) and input.d
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "group operand, assignment rewrite",
|
|
module: `package logic
|
|
p if {
|
|
x := 1
|
|
y := 2
|
|
z := 3
|
|
(x or y) and z
|
|
}
|
|
`,
|
|
expMod: `package logic
|
|
p = true if {
|
|
__local0__ = 1
|
|
__local1__ = 2
|
|
__local2__ = 3
|
|
(__local0__ or __local1__) and __local2__
|
|
}
|
|
`,
|
|
},
|
|
{
|
|
note: "not group",
|
|
module: `package logic
|
|
import future.keywords.not
|
|
p if {
|
|
not (input.a or input.b)
|
|
}
|
|
`,
|
|
expMod: MustParseModuleWithOpts(`package logic
|
|
p = true if {
|
|
not (input.a or input.b)
|
|
}
|
|
`, ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or", "not"},
|
|
}),
|
|
},
|
|
{
|
|
note: "standalone group, unsafe operands caught",
|
|
module: `package logic
|
|
p if {
|
|
(x or y)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var x is unsafe",
|
|
},
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var y is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "nested unsafe var found deep in group",
|
|
module: `package logic
|
|
p if {
|
|
input.a and (input.b or (input.c and z))
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{
|
|
Code: CompileErr,
|
|
Message: "var z is unsafe",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
note: "unsafe operands found on both group sides",
|
|
module: `package logic
|
|
p if {
|
|
(x or y) and (u or v)
|
|
}
|
|
`,
|
|
expErrs: Errors{
|
|
&Error{Code: CompileErr, Message: "var x is unsafe"},
|
|
&Error{Code: CompileErr, Message: "var y is unsafe"},
|
|
&Error{Code: CompileErr, Message: "var u is unsafe"},
|
|
&Error{Code: CompileErr, Message: "var v is unsafe"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{"mod.rego": MustParseModuleWithOpts(tc.module, popts)})
|
|
|
|
if len(tc.expErrs) > 0 {
|
|
assertErrors(t, c.Errors, tc.expErrs, false)
|
|
} else if len(c.Errors) > 0 {
|
|
if c.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c.Errors)
|
|
}
|
|
}
|
|
|
|
if tc.expMod != nil && tc.expMod != "" {
|
|
var expMod *Module
|
|
if m, ok := tc.expMod.(*Module); ok {
|
|
expMod = m
|
|
} else {
|
|
expMod = MustParseModuleWithOpts(tc.expMod.(string), popts)
|
|
}
|
|
|
|
if diff := cmp.Diff(expMod, c.Modules["mod.rego"]); diff != "" {
|
|
t.Errorf("unexpected module (-want, +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
for n, m := range c.Modules {
|
|
t.Logf("compiled module %s:\n\n%v\n\n%s", n, m, mermaidGraph(m))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerLogicalGroupRewrites(t *testing.T) {
|
|
popts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
FutureKeywords: []string{"and", "or", "not"},
|
|
}
|
|
|
|
tests := []struct {
|
|
note string
|
|
module string
|
|
expBody string
|
|
}{
|
|
{
|
|
note: "group operand rewritten to multi-expr body (lhs)",
|
|
module: `package logic
|
|
p if {
|
|
(startswith(lower(input.x), "a") or input.b)
|
|
}
|
|
`,
|
|
expBody: `{
|
|
__local1__ = input.x
|
|
lower(__local1__, __local0__)
|
|
startswith(__local0__, "a")
|
|
} or input.b`,
|
|
},
|
|
{
|
|
note: "group operand rewritten to multi-expr body (rhs)",
|
|
module: `package logic
|
|
p if {
|
|
input.b or (contains(lower(input.x), "z"))
|
|
}
|
|
`,
|
|
expBody: `input.b or {
|
|
__local1__ = input.x
|
|
lower(__local1__, __local0__)
|
|
contains(__local0__, "z")
|
|
}`,
|
|
},
|
|
{
|
|
note: "with on group operand survives, value hoisted",
|
|
module: `package logic
|
|
p if {
|
|
(input.ok with input as {"x": count(input.items)}) or input.b
|
|
}
|
|
`,
|
|
expBody: `{
|
|
__local1__ = input.items
|
|
count(__local1__, __local0__)
|
|
input.ok with input as {"x": __local0__}
|
|
} or input.b`,
|
|
},
|
|
{
|
|
note: "with on whole group survives, value hoisted to outer body",
|
|
module: `package logic
|
|
p if {
|
|
(input.a or input.b with input as {"x": count(input.items)})
|
|
}
|
|
`,
|
|
expBody: `__local1__ = input.items
|
|
count(__local1__, __local0__)
|
|
input.a or input.b with input as {"x": __local0__}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
c := NewCompiler()
|
|
c.Compile(map[string]*Module{"mod.rego": MustParseModuleWithOpts(tc.module, popts)})
|
|
if c.Failed() {
|
|
t.Fatalf("unexpected compile errors: %v", c.Errors)
|
|
}
|
|
got := c.Modules["mod.rego"].Rules[0].Body
|
|
want := MustParseBodyWithOpts(tc.expBody, popts)
|
|
if !got.Equal(want) {
|
|
t.Errorf("compiled body:\n want: %s\n got: %s", want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQueryCompilerAndOrImports(t *testing.T) {
|
|
popts := ParserOptions{
|
|
Capabilities: CapabilitiesForThisVersion(CapabilitiesExperimentalKeywords(true)),
|
|
AllFutureKeywords: true,
|
|
}
|
|
c := NewCompiler()
|
|
|
|
tests := []struct {
|
|
note string
|
|
query string
|
|
}{
|
|
{"and basic", "input.x and input.y"},
|
|
{"or basic", "input.x or input.y"},
|
|
{"explicit body with internal local", "{x := 1; x > 0} and true"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
body, err := ParseBodyWithOpts(tc.query, popts)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
qc := c.QueryCompiler()
|
|
if _, err := qc.Compile(body); err != nil {
|
|
t.Fatalf("query compile failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|