mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-22 08:14:48 -06:00
954196a690
The formatter would normally just use the stringer for ast.Var which would swap in `_` for any wildcard variables (internally represented with a `$xx` syntax). This works fine except for AST dumped into the formatter that might have the same wildcard variable used multiple times. This can be seen by using partial evaluation creating multiple statements from a single original source. In the formatted output if we swap in `_` it can affect the resulting logic if they were supposed to be the same variable. The formatter now will check for any wild cards that show up >1 time in the AST passed in to be formatted. Any it finds will be assigned a new variable name like `__wilcardxx__` and in the resulting output will use that name instead of the `_` syntax. Fixes: #2053 Signed-off-by: Patrick East <east.patrick@gmail.com>
292 lines
6.9 KiB
Go
292 lines
6.9 KiB
Go
// Copyright 2017 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 format
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
)
|
|
|
|
func TestFormatNilLocation(t *testing.T) {
|
|
rule := ast.MustParseRule(`r = y { y = "foo" }`)
|
|
rule.Head.Location = nil
|
|
|
|
bs, err := Ast(rule)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
exp := strings.Trim(`
|
|
r = y {
|
|
y = "foo"
|
|
}`, " \n")
|
|
|
|
if string(bs) != exp {
|
|
t.Fatalf("Expected %q but got %q", exp, string(bs))
|
|
}
|
|
}
|
|
|
|
func TestFormatNilLocationEmptyBody(t *testing.T) {
|
|
b := ast.NewBody()
|
|
x, err := Ast(b)
|
|
if len(x) != 0 || err != nil {
|
|
t.Fatalf("Expected empty result but got: %q, err: %v", string(x), err)
|
|
}
|
|
}
|
|
|
|
func TestFormatSourceError(t *testing.T) {
|
|
rego := "testfiles/test.rego.error"
|
|
contents, err := ioutil.ReadFile(rego)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read rego source: %v", err)
|
|
}
|
|
|
|
_, err = Source(rego, contents)
|
|
if err == nil {
|
|
t.Fatal("Expected parsing error, not nil")
|
|
}
|
|
|
|
exp := "1 error occurred: testfiles/test.rego.error:27: rego_parse_error: unexpected eof token"
|
|
|
|
if !strings.HasPrefix(err.Error(), exp) {
|
|
t.Fatalf("Expected error message '%s', got '%s'", exp, err.Error())
|
|
}
|
|
}
|
|
|
|
func TestFormatSource(t *testing.T) {
|
|
regoFiles, err := filepath.Glob("testfiles/*.rego")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, rego := range regoFiles {
|
|
t.Run(rego, func(t *testing.T) {
|
|
contents, err := ioutil.ReadFile(rego)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read rego source: %v", err)
|
|
}
|
|
|
|
expected, err := ioutil.ReadFile(rego + ".formatted")
|
|
if err != nil {
|
|
t.Fatalf("Failed to read expected rego source: %v", err)
|
|
}
|
|
|
|
formatted, err := Source(rego, contents)
|
|
if err != nil {
|
|
t.Fatalf("Failed to format file: %v", err)
|
|
}
|
|
|
|
if ln, at := differsAt(formatted, expected); ln != 0 {
|
|
t.Fatalf("Expected formatted bytes to equal expected bytes but differed near line %d / byte %d (got: %q, expected: %q):\n%s", ln, at, formatted[at], expected[at], prefixWithLineNumbers(formatted))
|
|
}
|
|
|
|
if _, err := ast.ParseModule(rego+".tmp", string(formatted)); err != nil {
|
|
t.Fatalf("Failed to parse formatted bytes: %v", err)
|
|
}
|
|
|
|
formatted, err = Source(rego, formatted)
|
|
if err != nil {
|
|
t.Fatalf("Failed to double format file")
|
|
}
|
|
|
|
if ln, at := differsAt(formatted, expected); ln != 0 {
|
|
t.Fatalf("Expected roundtripped bytes to equal expected bytes but differed near line %d / byte %d:\n%s", ln, at, prefixWithLineNumbers(formatted))
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatAST(t *testing.T) {
|
|
cases := []struct {
|
|
note string
|
|
toFmt interface{}
|
|
expected string
|
|
}{
|
|
{
|
|
note: "var",
|
|
toFmt: ast.Var(`foo`),
|
|
expected: "foo",
|
|
},
|
|
{
|
|
note: "string",
|
|
toFmt: &ast.Term{
|
|
Value: ast.String("foo"),
|
|
Location: &ast.Location{Text: []byte(`"foo"`)},
|
|
},
|
|
expected: `"foo"`,
|
|
},
|
|
{
|
|
note: "var wildcard",
|
|
toFmt: ast.Var(`$12`),
|
|
expected: "_",
|
|
},
|
|
{
|
|
note: "string with wildcard prefix",
|
|
toFmt: &ast.Term{
|
|
Value: ast.String("$01"),
|
|
Location: &ast.Location{Text: []byte(`"$01"`)},
|
|
},
|
|
expected: `"$01"`,
|
|
},
|
|
{
|
|
note: "ref var only",
|
|
toFmt: ast.MustParseRef(`data.foo`),
|
|
expected: "data.foo",
|
|
},
|
|
{
|
|
note: "ref multi vars",
|
|
toFmt: ast.MustParseRef(`data.foo.bar.baz`),
|
|
expected: "data.foo.bar.baz",
|
|
},
|
|
{
|
|
note: "ref with string",
|
|
toFmt: ast.MustParseRef(`data["foo"]`),
|
|
expected: `data.foo`,
|
|
},
|
|
{
|
|
note: "ref multi string",
|
|
toFmt: ast.MustParseRef(`data["foo"]["bar"]["baz"]`),
|
|
expected: `data.foo.bar.baz`,
|
|
},
|
|
{
|
|
note: "ref with string needs brackets",
|
|
toFmt: ast.MustParseRef(`data["foo my-var\nbar"]`),
|
|
expected: `data["foo my-var\nbar"]`,
|
|
},
|
|
{
|
|
note: "ref multi string needs brackets",
|
|
toFmt: ast.MustParseRef(`data["foo my-var"]["bar"]["almost.baz"]`),
|
|
expected: `data["foo my-var"].bar["almost.baz"]`,
|
|
},
|
|
{
|
|
note: "ref var wildcard",
|
|
toFmt: ast.MustParseRef(`data.foo[_]`),
|
|
expected: "data.foo[_]",
|
|
},
|
|
{
|
|
note: "ref var wildcard",
|
|
toFmt: ast.MustParseRef(`foo[_]`),
|
|
expected: "foo[_]",
|
|
},
|
|
{
|
|
note: "ref string with wildcard prefix",
|
|
toFmt: ast.MustParseRef(`foo["$01"]`),
|
|
expected: `foo["$01"]`,
|
|
},
|
|
{
|
|
note: "nested ref var wildcard",
|
|
toFmt: ast.MustParseRef(`foo[bar[baz[_]]]`),
|
|
expected: "foo[bar[baz[_]]]",
|
|
},
|
|
{
|
|
note: "ref mixed",
|
|
toFmt: ast.MustParseRef(`foo["bar"].baz[_]["bar-2"].qux`),
|
|
expected: `foo.bar.baz[_]["bar-2"].qux`,
|
|
},
|
|
{
|
|
note: "ref empty",
|
|
toFmt: ast.Ref{},
|
|
expected: ``,
|
|
},
|
|
{
|
|
note: "ref nil",
|
|
toFmt: ast.Ref(nil),
|
|
expected: ``,
|
|
},
|
|
{
|
|
note: "body shared wildcard",
|
|
toFmt: ast.Body{
|
|
&ast.Expr{
|
|
Index: 0,
|
|
Terms: []*ast.Term{
|
|
ast.RefTerm(ast.VarTerm("eq")),
|
|
ast.RefTerm(ast.VarTerm("input"), ast.StringTerm("arr"), ast.VarTerm("$01"), ast.StringTerm("some key"), ast.VarTerm("$02")),
|
|
ast.VarTerm("bar"),
|
|
},
|
|
},
|
|
&ast.Expr{
|
|
Index: 1,
|
|
Location: &ast.Location{
|
|
Row: 2,
|
|
Col: 1,
|
|
},
|
|
Terms: []*ast.Term{
|
|
ast.RefTerm(ast.VarTerm("eq")),
|
|
ast.RefTerm(ast.VarTerm("input"), ast.StringTerm("arr"), ast.VarTerm("$01"), ast.StringTerm("bar")),
|
|
ast.VarTerm("qux"),
|
|
},
|
|
},
|
|
&ast.Expr{
|
|
Index: 1,
|
|
Location: &ast.Location{
|
|
Row: 2,
|
|
Col: 1,
|
|
},
|
|
Terms: []*ast.Term{
|
|
ast.RefTerm(ast.VarTerm("eq")),
|
|
ast.RefTerm(ast.VarTerm("foo"), ast.VarTerm("$03"), ast.VarTerm("$01"), ast.StringTerm("bar")),
|
|
ast.RefTerm(ast.VarTerm("bar"), ast.VarTerm("$03"), ast.VarTerm("$04"), ast.VarTerm("$01"), ast.StringTerm("bar")),
|
|
},
|
|
},
|
|
},
|
|
expected: `input.arr[__wildcard0__]["some key"][_] = bar
|
|
input.arr[__wildcard0__].bar = qux
|
|
foo[__wildcard1__][__wildcard0__].bar = bar[__wildcard1__][_][__wildcard0__].bar
|
|
`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.note, func(t *testing.T) {
|
|
bs, err := Ast(tc.toFmt)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
expected := strings.TrimSpace(tc.expected)
|
|
actual := strings.TrimSpace(string(bs))
|
|
if actual != expected {
|
|
t.Fatalf("Expected:\n\n%s\n\nGot:\n\n%s\n\n", expected, actual)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func differsAt(a, b []byte) (int, int) {
|
|
if bytes.Equal(a, b) {
|
|
return 0, 0
|
|
}
|
|
minLen := len(a)
|
|
if minLen > len(b) {
|
|
minLen = len(b)
|
|
}
|
|
ln := 1
|
|
for i := 0; i < minLen; i++ {
|
|
if a[i] == '\n' {
|
|
ln++
|
|
}
|
|
if a[i] != b[i] {
|
|
return ln, i
|
|
}
|
|
}
|
|
return ln, minLen - 1
|
|
}
|
|
|
|
func prefixWithLineNumbers(bs []byte) []byte {
|
|
raw := string(bs)
|
|
lines := strings.Split(raw, "\n")
|
|
format := fmt.Sprintf("%%%dd %%s", len(fmt.Sprint(len(lines)+1)))
|
|
for i, line := range lines {
|
|
lines[i] = fmt.Sprintf(format, i+1, line)
|
|
}
|
|
return []byte(strings.Join(lines, "\n"))
|
|
}
|