![](/img/logos/styra-logo.png)
![](/img/logos/paclabs-logo.png)
diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 9026314313..1bd9f08394 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -2,7 +2,7 @@ name: Nightly on: workflow_dispatch: {} # Allow for manual triggers schedule: - - cron: '0 8 * * *' # Daily, at 8:00 UTC + - cron: '0 8 * * 0-4' # Sun-Thu, at 8:00 UTC jobs: diff --git a/ast/annotations.go b/ast/annotations.go index 1134cc87c4..fe5e321997 100644 --- a/ast/annotations.go +++ b/ast/annotations.go @@ -11,6 +11,7 @@ import ( "sort" "strings" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/internal/deepcopy" "github.com/open-policy-agent/opa/util" ) @@ -39,7 +40,7 @@ type ( comments []*Comment node Node - jsonOptions JSONOptions + jsonOptions astJSON.Options } // SchemaAnnotation contains a schema declaration for the document identified by the path. @@ -76,7 +77,7 @@ type ( Annotations *Annotations `json:"annotations,omitempty"` Location *Location `json:"location,omitempty"` // The location of the node the annotations are applied to - jsonOptions JSONOptions + jsonOptions astJSON.Options node Node // The node the annotations are applied to } @@ -180,8 +181,11 @@ func (a *Annotations) GetTargetPath() Ref { } } -func (a *Annotations) setJSONOptions(opts JSONOptions) { +func (a *Annotations) setJSONOptions(opts astJSON.Options) { a.jsonOptions = opts + if a.Location != nil { + a.Location.JSONOptions = opts + } } func (a *Annotations) MarshalJSON() ([]byte, error) { diff --git a/ast/compile.go b/ast/compile.go index 28392cade0..b577d9e47d 100644 --- a/ast/compile.go +++ b/ast/compile.go @@ -7,7 +7,6 @@ package ast import ( "fmt" "io" - "os" "sort" "strconv" "strings" @@ -146,7 +145,6 @@ type Compiler struct { keepModules bool // whether to keep the unprocessed, parse modules (below) parsedModules map[string]*Module // parsed, but otherwise unprocessed modules, kept track of when keepModules is true useTypeCheckAnnotations bool // whether to provide annotated information (schemas) to the type checker - generalRuleRefsEnabled bool } // CompilerStage defines the interface for stages in the compiler. @@ -335,8 +333,6 @@ func NewCompiler() *Compiler { {"BuildComprehensionIndices", "compile_stage_rebuild_comprehension_indices", c.buildComprehensionIndices}, } - _, c.generalRuleRefsEnabled = os.LookupEnv("EXPERIMENTAL_GENERAL_RULE_REFS") - return c } @@ -1005,50 +1001,8 @@ func (c *Compiler) checkRuleConflicts() { // data.p.q[r][s] { r := input.r; s := input.s } // data.p[q].r.s { q := input.q } - if c.generalRuleRefsEnabled { - if r.Ref().IsGround() && len(node.Children) > 0 { - conflicts = node.flattenChildren() - } - } else { // TODO: Remove when general rule refs are enabled by default. - if r.Head.RuleKind() == SingleValue && len(node.Children) > 0 { - if len(ref) > 1 && !ref[len(ref)-1].IsGround() { // p.q[x] and p.q.s.t => check grandchildren - for _, c := range node.Children { - grandchildrenFound := false - - if len(c.Values) > 0 { - childRules := extractRules(c.Values) - for _, childRule := range childRules { - childRef := childRule.Ref() - if childRule.Head.RuleKind() == SingleValue && !childRef[len(childRef)-1].IsGround() { - // The child is a partial object rule, so it's effectively "generating" grandchildren. - grandchildrenFound = true - break - } - } - } - - if len(c.Children) > 0 { - grandchildrenFound = true - } - - if grandchildrenFound { - conflicts = node.flattenChildren() - break - } - } - } else { // p.q.s and p.q.s.t => any children are in conflict - conflicts = node.flattenChildren() - } - } - - // Multi-value rules may not have any other rules in their extent; e.g.: - // - // data.p[v] { v := ... } - // data.p.q := 42 # In direct conflict with data.p[v], which is constructing a set and cannot have values assigned to a sub-path. - - if r.Head.RuleKind() == MultiValue && len(node.Children) > 0 { - conflicts = node.flattenChildren() - } + if r.Ref().IsGround() && len(node.Children) > 0 { + conflicts = node.flattenChildren() } if r.Head.RuleKind() == SingleValue && r.Head.Ref().IsGround() { @@ -1811,18 +1765,6 @@ func (c *Compiler) rewriteRuleHeadRefs() { } for i := 1; i < len(ref); i++ { - // NOTE: Unless enabled via the EXPERIMENTAL_GENERAL_RULE_REFS env var, non-string values in the refs are forbidden - // except for the last position, e.g. - // OK: p.q.r[s] - // NOT OK: p[q].r.s - // TODO: Remove when general rule refs are enabled by default. - if !c.generalRuleRefsEnabled && i != len(ref)-1 { // last - if _, ok := ref[i].Value.(String); !ok { - c.err(NewError(TypeErr, rule.Loc(), "rule head must only contain string terms (except for last): %v", ref[i])) - continue - } - } - // Rewrite so that any non-scalar elements that in the last position of // the rule are vars: // p.q.r[y.z] { ... } => p.q.r[__local0__] { __local0__ = y.z } @@ -2385,8 +2327,9 @@ func (c *Compiler) rewriteLocalVarsInRule(rule *Rule, unusedArgs VarSet, argsSta // Rewrite assignments in body. used := NewVarSet() - last := rule.Head.Ref()[len(rule.Head.Ref())-1] - used.Update(last.Vars()) + for _, t := range rule.Head.Ref()[1:] { + used.Update(t.Vars()) + } if rule.Head.Key != nil { used.Update(rule.Head.Key.Vars()) diff --git a/ast/compile_test.go b/ast/compile_test.go index 2dcdf75ab8..2fb3532494 100644 --- a/ast/compile_test.go +++ b/ast/compile_test.go @@ -467,8 +467,6 @@ func toRef(s string) Ref { } func TestCompilerCheckRuleHeadRefs(t *testing.T) { - t.Setenv("EXPERIMENTAL_GENERAL_RULE_REFS", "true") - tests := []struct { note string modules []*Module @@ -607,64 +605,6 @@ func TestCompilerCheckRuleHeadRefs(t *testing.T) { } } -// TODO: Remove when general rule refs are enabled by default. -func TestCompilerCheckRuleHeadRefsWithGeneralRuleRefsDisabled(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 { i := 10 }`, - ), - err: "rego_type_error: rule head must only contain string terms (except for last): i", - }, - { - note: "invalid: ref in ref", - modules: modules( - `package x - p.q[arr[0]].r { i := 10 }`, - ), - err: "rego_type_error: rule head must only contain string terms (except for last): arr[0]", - }, - { - note: "invalid: non-string in ref (not last position)", - modules: modules( - `package x - p.q[10].r { true }`, - ), - err: "rego_type_error: rule head must only contain string terms (except for last): 10", - }, - } - - 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[fmt.Sprint(i)] = m - } - c := NewCompiler() - c.Modules = mods - compileStages(c, c.rewriteRuleHeadRefs) - 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 @@ -1940,8 +1880,6 @@ func TestCompilerCheckRuleConflictsDefaultFunction(t *testing.T) { } func TestCompilerCheckRuleConflictsDotsInRuleHeads(t *testing.T) { - t.Setenv("EXPERIMENTAL_GENERAL_RULE_REFS", "true") - tests := []struct { note string modules []*Module @@ -2187,70 +2125,6 @@ func TestCompilerCheckRuleConflictsDotsInRuleHeads(t *testing.T) { } } -// TODO: Remove when general rule refs are enabled by default. -func TestGeneralRuleRefsDisabled(t *testing.T) { - // EXPERIMENTAL_GENERAL_RULE_REFS env var not set - - tests := []struct { - note string - modules []*Module - err string - }{ - { - note: "single-value with other rule overlap, unknown key", - modules: modules( - `package pkg - p.q[r] = x { r = input.key; x = input.foo } - p.q.r.s = x { true } - `), - err: "rego_type_error: rule data.pkg.p.q[r] conflicts with [data.pkg.p.q.r.s]", - }, - { - note: "single-value with other rule overlap, unknown ref var and key", - modules: modules( - `package pkg - p.q[r][s] = x { r = input.key1; s = input.key2; x = input.foo } - p.q.r.s.t = x { true } - `), - err: "rego_type_error: rule head must only contain string terms (except for last): r", - }, - { - 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 { r = input.key; x = input.bar } - p.q[r] := x { r = input.key; x = input.bar } - `), - err: "rego_type_error: rule data.pkg.p[r] conflicts with [data.pkg.p.q[r]]", - }, - { - note: "single-value partial object with other partial object (implicit 'true' value) rule overlap, unknown keys", - modules: modules( - `package pkg - p[r] := x { r = input.key; x = input.bar } - p.q[r] { r = input.key } - `), - err: "rego_type_error: rule data.pkg.p[r] conflicts with [data.pkg.p.q[r]]", - }, - } - 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[fmt.Sprint(i)] = m - } - c := NewCompiler() - c.Modules = mods - compileStages(c, c.checkRuleConflicts) - if tc.err != "" { - assertCompilerErrorStrings(t, c, []string{tc.err}) - } else { - assertCompilerErrorStrings(t, c, []string{}) - } - }) - } -} - func TestCompilerCheckUndefinedFuncs(t *testing.T) { module := ` @@ -7278,6 +7152,29 @@ func TestCompilerCheckUnusedAssignedVar(t *testing.T) { &Error{Message: "assigned var y unused"}, }, }, + { + note: "general ref in rule head", + module: `package test + p[q].r[s] := 1 { + 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 { + q := "foo" + s := "bar" + } + `, + expectedErrors: Errors{}, + }, } makeTestRunner := func(tc testCase, strict bool) func(t *testing.T) { diff --git a/ast/json/json.go b/ast/json/json.go new file mode 100644 index 0000000000..2a72a9dbd6 --- /dev/null +++ b/ast/json/json.go @@ -0,0 +1,33 @@ +package json + +// Options defines the options for JSON operations, +// currently only marshaling can be configured +type Options struct { + MarshalOptions MarshalOptions +} + +// MarshalOptions defines the options for JSON marshaling, +// currently only toggling the marshaling of location information is supported +type MarshalOptions struct { + // IncludeLocation toggles the marshaling of location information + IncludeLocation NodeToggle + // IncludeLocationText additionally/optionally includes the text of the location + IncludeLocationText bool +} + +// NodeToggle is a generic struct to allow the toggling of +// settings for different ast node types +type NodeToggle struct { + Term bool + Package bool + Comment bool + Import bool + Rule bool + Head bool + Expr bool + SomeDecl bool + Every bool + With bool + Annotations bool + AnnotationsRef bool +} diff --git a/ast/location/location.go b/ast/location/location.go index 9ef1e6dfd4..1ac4d15282 100644 --- a/ast/location/location.go +++ b/ast/location/location.go @@ -3,8 +3,11 @@ package location import ( "bytes" + "encoding/json" "errors" "fmt" + + astJSON "github.com/open-policy-agent/opa/ast/json" ) // Location records a position in source code @@ -14,6 +17,9 @@ type Location struct { Row int `json:"row"` // The line in the source. Col int `json:"col"` // The column in the row. Offset int `json:"-"` // The byte offset for the location in the source. + + // JSONOptions specifies options for marshaling and unmarshaling of locations + JSONOptions astJSON.Options } // NewLocation returns a new Location object. @@ -87,3 +93,23 @@ func (loc *Location) Compare(other *Location) int { } return 0 } + +func (loc *Location) MarshalJSON() ([]byte, error) { + // structs are used here to preserve the field ordering of the original Location struct + data := struct { + File string `json:"file"` + Row int `json:"row"` + Col int `json:"col"` + Text []byte `json:"text,omitempty"` + }{ + File: loc.File, + Row: loc.Row, + Col: loc.Col, + } + + if loc.JSONOptions.MarshalOptions.IncludeLocationText { + data.Text = loc.Text + } + + return json.Marshal(data) +} diff --git a/ast/location/location_test.go b/ast/location/location_test.go index a6e54c62d1..f61fec5edd 100644 --- a/ast/location/location_test.go +++ b/ast/location/location_test.go @@ -1,8 +1,10 @@ package location import ( + "encoding/json" "testing" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/util" ) @@ -85,3 +87,46 @@ func TestLocationCompare(t *testing.T) { } } } + +func TestLocationMarshal(t *testing.T) { + testCases := map[string]struct { + loc *Location + exp string + }{ + "default json options": { + loc: &Location{ + Text: []byte("text"), + File: "file", + Row: 1, + Col: 1, + }, + exp: `{"file":"file","row":1,"col":1}`, + }, + "including text": { + loc: &Location{ + Text: []byte("text"), + File: "file", + Row: 1, + Col: 1, + JSONOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocationText: true, + }, + }, + }, + exp: `{"file":"file","row":1,"col":1,"text":"dGV4dA=="}`, + }, + } + + for id, tc := range testCases { + t.Run(id, func(t *testing.T) { + bs, err := json.Marshal(tc.loc) + if err != nil { + t.Fatal(err) + } + if string(bs) != tc.exp { + t.Fatalf("Expected %v but got %v", tc.exp, string(bs)) + } + }) + } +} diff --git a/ast/marshal.go b/ast/marshal.go index 891945db8b..53fb112044 100644 --- a/ast/marshal.go +++ b/ast/marshal.go @@ -1,7 +1,11 @@ package ast +import ( + astJSON "github.com/open-policy-agent/opa/ast/json" +) + // customJSON is an interface that can be implemented by AST nodes that // allows the parser to set options for JSON operations on that node. type customJSON interface { - setJSONOptions(JSONOptions) + setJSONOptions(astJSON.Options) } diff --git a/ast/marshal_test.go b/ast/marshal_test.go index 38b5216279..d6410102f6 100644 --- a/ast/marshal_test.go +++ b/ast/marshal_test.go @@ -4,9 +4,79 @@ import ( "encoding/json" "testing" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/util" ) +func TestGeneric_MarshalWithLocationJSONOptions(t *testing.T) { + testCases := map[string]struct { + Term *Term + ExpectedJSON string + }{ + "base case, no location options set": { + Term: func() *Term { + v, _ := InterfaceToValue("example") + return &Term{ + Value: v, + Location: NewLocation([]byte{}, "example.rego", 1, 2), + } + }(), + ExpectedJSON: `{"type":"string","value":"example"}`, + }, + "location included, location text excluded": { + Term: func() *Term { + v, _ := InterfaceToValue("example") + return &Term{ + Value: v, + Location: NewLocation([]byte{}, "example.rego", 1, 2), + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ + Term: true, + }, + IncludeLocationText: false, + }, + }, + } + }(), + ExpectedJSON: `{"location":{"file":"example.rego","row":1,"col":2},"type":"string","value":"example"}`, + }, + "location included, location text also included": { + Term: func() *Term { + v, _ := InterfaceToValue("example") + t := &Term{ + Value: v, + Location: NewLocation([]byte("things"), "example.rego", 1, 2), + } + t.setJSONOptions( + astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ + Term: true, + }, + IncludeLocationText: true, + }, + }, + ) + return t + }(), + ExpectedJSON: `{"location":{"file":"example.rego","row":1,"col":2,"text":"dGhpbmdz"},"type":"string","value":"example"}`, + }, + } + + for name, data := range testCases { + t.Run(name, func(t *testing.T) { + bs := util.MustMarshalJSON(data.Term) + got := string(bs) + exp := data.ExpectedJSON + + if got != exp { + t.Fatalf("expected:\n%s got\n%s", exp, got) + } + }) + } +} + func TestTerm_MarshalJSON(t *testing.T) { testCases := map[string]struct { Term *Term @@ -28,9 +98,9 @@ func TestTerm_MarshalJSON(t *testing.T) { return &Term{ Value: v, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Term: false, }, }, @@ -45,9 +115,9 @@ func TestTerm_MarshalJSON(t *testing.T) { return &Term{ Value: v, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Term: true, }, }, @@ -132,9 +202,9 @@ func TestPackage_MarshalJSON(t *testing.T) { Package: &Package{ Path: EmptyRef(), Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Package: false, }, }, @@ -146,9 +216,9 @@ func TestPackage_MarshalJSON(t *testing.T) { Package: &Package{ Path: EmptyRef(), Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Package: true, }, }, @@ -188,9 +258,9 @@ func TestComment_MarshalJSON(t *testing.T) { Comment: &Comment{ Text: []byte("comment"), Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Comment: false, // ignored }, }, @@ -202,9 +272,9 @@ func TestComment_MarshalJSON(t *testing.T) { Comment: &Comment{ Text: []byte("comment"), Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Comment: true, // ignored }, }, @@ -253,9 +323,9 @@ func TestImport_MarshalJSON(t *testing.T) { return &Import{ Path: &term, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Import: false, }, }, @@ -274,9 +344,9 @@ func TestImport_MarshalJSON(t *testing.T) { return &Import{ Path: &term, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Import: true, }, }, @@ -327,9 +397,9 @@ func TestRule_MarshalJSON(t *testing.T) { "location excluded": { Rule: func() *Rule { r := rule.Copy() - r.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + r.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Rule: false, }, }, @@ -341,9 +411,9 @@ func TestRule_MarshalJSON(t *testing.T) { "location included": { Rule: func() *Rule { r := rule.Copy() - r.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + r.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Rule: true, }, }, @@ -394,9 +464,9 @@ func TestHead_MarshalJSON(t *testing.T) { "location excluded": { Head: func() *Head { h := head.Copy() - h.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + h.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Head: false, }, }, @@ -409,9 +479,9 @@ func TestHead_MarshalJSON(t *testing.T) { "location included": { Head: func() *Head { h := head.Copy() - h.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + h.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Head: true, }, }, @@ -462,9 +532,9 @@ func TestExpr_MarshalJSON(t *testing.T) { "location excluded": { Expr: func() *Expr { e := expr.Copy() - e.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + e.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Expr: false, }, }, @@ -477,9 +547,9 @@ func TestExpr_MarshalJSON(t *testing.T) { "location included": { Expr: func() *Expr { e := expr.Copy() - e.jsonOptions = JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + e.jsonOptions = astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Expr: true, }, }, @@ -581,7 +651,7 @@ func TestSomeDecl_MarshalJSON(t *testing.T) { SomeDecl: &SomeDecl{ Symbols: []*Term{term}, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{SomeDecl: false}}}, + jsonOptions: astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{SomeDecl: false}}}, }, ExpectedJSON: `{"symbols":[{"type":"string","value":"example"}]}`, }, @@ -589,7 +659,7 @@ func TestSomeDecl_MarshalJSON(t *testing.T) { SomeDecl: &SomeDecl{ Symbols: []*Term{term}, Location: NewLocation([]byte{}, "example.rego", 1, 2), - jsonOptions: JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{SomeDecl: true}}}, + jsonOptions: astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{SomeDecl: true}}}, }, ExpectedJSON: `{"location":{"file":"example.rego","row":1,"col":2},"symbols":[{"type":"string","value":"example"}]}`, }, @@ -643,7 +713,7 @@ allow { "location excluded": { Every: func() *Every { e := every.Copy() - e.jsonOptions = JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{Every: false}}} + e.jsonOptions = astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{Every: false}}} return e }(), ExpectedJSON: `{"body":[{"index":0,"terms":[{"type":"ref","value":[{"type":"var","value":"equal"}]},{"type":"var","value":"e"},{"type":"number","value":1}]}],"domain":{"type":"array","value":[{"type":"number","value":1},{"type":"number","value":2},{"type":"number","value":3}]},"key":null,"value":{"type":"var","value":"e"}}`, @@ -651,7 +721,7 @@ allow { "location included": { Every: func() *Every { e := every.Copy() - e.jsonOptions = JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{Every: true}}} + e.jsonOptions = astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{Every: true}}} return e }(), ExpectedJSON: `{"body":[{"index":0,"terms":[{"type":"ref","value":[{"type":"var","value":"equal"}]},{"type":"var","value":"e"},{"type":"number","value":1}]}],"domain":{"type":"array","value":[{"type":"number","value":1},{"type":"number","value":2},{"type":"number","value":3}]},"key":null,"location":{"file":"example.rego","row":7,"col":2},"value":{"type":"var","value":"e"}}`, @@ -701,7 +771,7 @@ b { "location excluded": { With: func() *With { w := with.Copy() - w.jsonOptions = JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{With: false}}} + w.jsonOptions = astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{With: false}}} return w }(), ExpectedJSON: `{"target":{"type":"ref","value":[{"type":"var","value":"input"}]},"value":{"type":"number","value":1}}`, @@ -709,7 +779,7 @@ b { "location included": { With: func() *With { w := with.Copy() - w.jsonOptions = JSONOptions{MarshalOptions: JSONMarshalOptions{IncludeLocation: NodeToggle{With: true}}} + w.jsonOptions = astJSON.Options{MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{With: true}}} return w }(), ExpectedJSON: `{"location":{"file":"example.rego","row":7,"col":4},"target":{"type":"ref","value":[{"type":"var","value":"input"}]},"value":{"type":"number","value":1}}`, @@ -761,9 +831,9 @@ func TestAnnotations_MarshalJSON(t *testing.T) { }, Location: NewLocation([]byte{}, "example.rego", 1, 4), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{Annotations: false}, + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{Annotations: false}, }, }, }, @@ -781,9 +851,9 @@ func TestAnnotations_MarshalJSON(t *testing.T) { }, Location: NewLocation([]byte{}, "example.rego", 1, 4), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{Annotations: true}, + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{Annotations: true}, }, }, }, @@ -824,9 +894,9 @@ func TestAnnotationsRef_MarshalJSON(t *testing.T) { Path: []*Term{}, Annotations: &Annotations{}, Location: NewLocation([]byte{}, "example.rego", 1, 4), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{AnnotationsRef: false}, + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{AnnotationsRef: false}, }, }, }, @@ -838,9 +908,9 @@ func TestAnnotationsRef_MarshalJSON(t *testing.T) { Annotations: &Annotations{}, Location: NewLocation([]byte{}, "example.rego", 1, 4), - jsonOptions: JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{AnnotationsRef: true}, + jsonOptions: astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{AnnotationsRef: true}, }, }, }, @@ -916,9 +986,9 @@ package test p = 1`, options: ParserOptions{ ProcessAnnotation: true, - JSONOptions: &JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + JSONOptions: &astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Term: true, Package: true, Comment: true, @@ -937,8 +1007,8 @@ p = 1`, }, expected: []string{ `{"annotations":{"authors":[{"name":"pkg"}],"custom":{"pkg":"pkg"},"description":"pkg","location":{"file":"","row":1,"col":1},"organizations":["pkg"],"related_resources":[{"ref":"https://pkg"}],"schemas":[{"path":[{"type":"var","value":"input"},{"type":"string","value":"foo"}],"definition":{"type":"boolean"}}],"scope":"package","title":"pkg"},"location":{"file":"","row":14,"col":1},"path":[{"location":{"file":"","row":14,"col":9},"type":"var","value":"data"},{"location":{"file":"","row":14,"col":9},"type":"string","value":"test"}]}`, - `{"annotations":{"authors":[{"name":"doc"}],"custom":{"doc":"doc"},"description":"doc","location":{"file":"","row":16,"col":1},"organizations":["doc"],"related_resources":[{"ref":"https://doc"}],"schemas":[{"path":[{"type":"var","value":"input"},{"type":"string","value":"bar"}],"definition":{"type":"integer"}}],"scope":"document","title":"doc"},"location":{"file":"","row":44,"col":1},"path":[{"location":{"file":"","row":14,"col":9},"type":"var","value":"data"},{"location":{"file":"","row":14,"col":9},"type":"string","value":"test"},{"type":"string","value":"p"}]}`, - `{"annotations":{"authors":[{"name":"rule"}],"custom":{"rule":"rule"},"description":"rule","location":{"file":"","row":31,"col":1},"organizations":["rule"],"related_resources":[{"ref":"https://rule"}],"schemas":[{"path":[{"type":"var","value":"input"},{"type":"string","value":"baz"}],"definition":{"type":"string"}}],"scope":"rule","title":"rule"},"location":{"file":"","row":44,"col":1},"path":[{"location":{"file":"","row":14,"col":9},"type":"var","value":"data"},{"location":{"file":"","row":14,"col":9},"type":"string","value":"test"},{"type":"string","value":"p"}]}`, + `{"annotations":{"authors":[{"name":"doc"}],"custom":{"doc":"doc"},"description":"doc","location":{"file":"","row":16,"col":1},"organizations":["doc"],"related_resources":[{"ref":"https://doc"}],"schemas":[{"path":[{"type":"var","value":"input"},{"type":"string","value":"bar"}],"definition":{"type":"integer"}}],"scope":"document","title":"doc"},"location":{"file":"","row":44,"col":1},"path":[{"location":{"file":"","row":14,"col":9},"type":"var","value":"data"},{"location":{"file":"","row":14,"col":9},"type":"string","value":"test"},{"location":{"file":"","row":44,"col":1},"type":"string","value":"p"}]}`, + `{"annotations":{"authors":[{"name":"rule"}],"custom":{"rule":"rule"},"description":"rule","location":{"file":"","row":31,"col":1},"organizations":["rule"],"related_resources":[{"ref":"https://rule"}],"schemas":[{"path":[{"type":"var","value":"input"},{"type":"string","value":"baz"}],"definition":{"type":"string"}}],"scope":"rule","title":"rule"},"location":{"file":"","row":44,"col":1},"path":[{"location":{"file":"","row":14,"col":9},"type":"var","value":"data"},{"location":{"file":"","row":14,"col":9},"type":"string","value":"test"},{"location":{"file":"","row":44,"col":1},"type":"string","value":"p"}]}`, }, }, } diff --git a/ast/parser.go b/ast/parser.go index 540d7b80c1..7cbc0b2892 100644 --- a/ast/parser.go +++ b/ast/parser.go @@ -11,7 +11,6 @@ import ( "io" "math/big" "net/url" - "os" "regexp" "sort" "strconv" @@ -22,6 +21,7 @@ import ( "github.com/open-policy-agent/opa/ast/internal/scanner" "github.com/open-policy-agent/opa/ast/internal/tokens" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/ast/location" ) @@ -97,43 +97,13 @@ func (e *parsedTermCacheItem) String() string { // ParserOptions defines the options for parsing Rego statements. type ParserOptions struct { - Capabilities *Capabilities - ProcessAnnotation bool - AllFutureKeywords bool - FutureKeywords []string - SkipRules bool - JSONOptions *JSONOptions - unreleasedKeywords bool // TODO(sr): cleanup - generalRuleRefsEnabled bool -} - -// JSONOptions defines the options for JSON operations, -// currently only marshaling can be configured -type JSONOptions struct { - MarshalOptions JSONMarshalOptions -} - -// JSONMarshalOptions defines the options for JSON marshaling, -// currently only toggling the marshaling of location information is supported -type JSONMarshalOptions struct { - IncludeLocation NodeToggle -} - -// NodeToggle is a generic struct to allow the toggling of -// settings for different ast node types -type NodeToggle struct { - Term bool - Package bool - Comment bool - Import bool - Rule bool - Head bool - Expr bool - SomeDecl bool - Every bool - With bool - Annotations bool - AnnotationsRef bool + Capabilities *Capabilities + ProcessAnnotation bool + AllFutureKeywords bool + FutureKeywords []string + SkipRules bool + JSONOptions *astJSON.Options + unreleasedKeywords bool // TODO(sr): cleanup } // NewParser creates and initializes a Parser. @@ -142,7 +112,6 @@ func NewParser() *Parser { s: &state{}, po: ParserOptions{}, } - _, p.po.generalRuleRefsEnabled = os.LookupEnv("EXPERIMENTAL_GENERAL_RULE_REFS") return p } @@ -210,9 +179,9 @@ func (p *Parser) WithSkipRules(skip bool) *Parser { return p } -// WithJSONOptions sets the JSONOptions which will be set on nodes to configure +// WithJSONOptions sets the Options which will be set on nodes to configure // their JSON marshaling behavior. -func (p *Parser) WithJSONOptions(jsonOptions *JSONOptions) *Parser { +func (p *Parser) WithJSONOptions(jsonOptions *astJSON.Options) *Parser { p.po.JSONOptions = jsonOptions return p } @@ -627,11 +596,6 @@ func (p *Parser) parseRules() []*Rule { return []*Rule{&rule} } - if !p.po.generalRuleRefsEnabled && usesContains && !rule.Head.Reference.IsGround() { - p.error(p.s.Loc(), "multi-value rules need ground refs") - return nil - } - // back-compat with `p[x] { ... }`` hasIf := p.s.tok == tokens.If @@ -795,43 +759,36 @@ func (p *Parser) parseElse(head *Head) *Rule { } hasIf := p.s.tok == tokens.If + hasLBrace := p.s.tok == tokens.LBrace - if hasIf { - p.scan() - s := p.save() - if expr := p.parseLiteral(); expr != nil { - // NOTE(sr): set literals are never false or undefined, so parsing this as - // p if false else if { true } - // ^^^^^^^^ set of one element, `true` - // isn't valid. - isSetLiteral := false - if t, ok := expr.Terms.(*Term); ok { - _, isSetLiteral = t.Value.(Set) - } - // expr.Term is []*Term or Every - if !isSetLiteral { - rule.Body.Append(expr) - setLocRecursive(rule.Body, rule.Location) - return &rule - } - } - p.restore(s) - } - - if p.s.tok != tokens.LBrace { + if !hasIf && !hasLBrace { rule.Body = NewBody(NewExpr(BooleanTerm(true))) setLocRecursive(rule.Body, rule.Location) return &rule } - p.scan() + if hasIf { + p.scan() + } - if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil { + if p.s.tok == tokens.LBrace { + p.scan() + if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil { + return nil + } + p.scan() + } else if p.s.tok != tokens.EOF { + expr := p.parseLiteral() + if expr == nil { + return nil + } + rule.Body.Append(expr) + setLocRecursive(rule.Body, rule.Location) + } else { + p.illegal("rule body expected") return nil } - p.scan() - if p.s.tok == tokens.Else { if rule.Else = p.parseElse(head); rule.Else == nil { return nil @@ -841,7 +798,6 @@ func (p *Parser) parseElse(head *Head) *Rule { } func (p *Parser) parseHead(defaultRule bool) (*Head, bool) { - head := &Head{} loc := p.s.Loc() defer func() { @@ -864,7 +820,9 @@ func (p *Parser) parseHead(defaultRule bool) (*Head, bool) { switch x := ref.Value.(type) { case Var: - head = NewHead(x) + // Modify the code to add the location to the head ref + // and set the head ref's jsonOptions. + head = VarHead(x, ref.Location, p.po.JSONOptions) case Ref: head = RefHead(x) case Call: diff --git a/ast/parser_ext.go b/ast/parser_ext.go index 99430a8fc5..9226a9059a 100644 --- a/ast/parser_ext.go +++ b/ast/parser_ext.go @@ -16,6 +16,8 @@ import ( "fmt" "strings" "unicode" + + astJSON "github.com/open-policy-agent/opa/ast/json" ) // MustParseBody returns a parsed body. @@ -244,7 +246,9 @@ func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, erro var head *Head if v, ok := lhs.Value.(Var); ok { - head = NewHead(v) + // Modify the code to add the location to the head ref + // and set the head ref's jsonOptions. + head = VarHead(v, lhs.Location, &lhs.jsonOptions) } else if r, ok := lhs.Value.(Ref); ok { // groundness ? if _, ok := r[0].Value.(Var); !ok { return nil, fmt.Errorf("invalid rule head: %v", r) @@ -350,7 +354,9 @@ func ParsePartialSetDocRuleFromTerm(module *Module, term *Term) (*Rule, error) { if !ok { return nil, fmt.Errorf("%vs cannot be used for rule head", TypeName(term.Value)) } - head = NewHead(v) + // Modify the code to add the location to the head ref + // and set the head ref's jsonOptions. + head = VarHead(v, ref[0].Location, &ref[0].jsonOptions) head.Key = ref[1] } head.Location = term.Location @@ -709,7 +715,7 @@ func setRuleModule(rule *Rule, module *Module) { } } -func setJSONOptions(x interface{}, jsonOptions *JSONOptions) { +func setJSONOptions(x interface{}, jsonOptions *astJSON.Options) { vis := NewGenericVisitor(func(x interface{}) bool { if x, ok := x.(customJSON); ok { x.setJSONOptions(*jsonOptions) diff --git a/ast/parser_test.go b/ast/parser_test.go index b4cb9951a9..7a2af2ae05 100644 --- a/ast/parser_test.go +++ b/ast/parser_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/open-policy-agent/opa/ast/internal/tokens" + astJSON "github.com/open-policy-agent/opa/ast/json" ) const ( @@ -3315,9 +3316,9 @@ func TestRuleFromBodyJSONOptions(t *testing.T) { } parserOpts := ParserOptions{ProcessAnnotation: true} - parserOpts.JSONOptions = &JSONOptions{ - MarshalOptions: JSONMarshalOptions{ - IncludeLocation: NodeToggle{ + parserOpts.JSONOptions = &astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocation: astJSON.NodeToggle{ Term: true, Package: true, Comment: true, @@ -5136,33 +5137,33 @@ func assertParseModuleJSONOptions(t *testing.T, msg string, input string, opts . rule := m.Rules[0] if rule.Head.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected rule Head JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.Head.jsonOptions) + t.Fatalf("Error on test \"%s\": expected rule Head Options\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.Head.jsonOptions) } if rule.Body[0].jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected rule Body JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.Body[0].jsonOptions) + t.Fatalf("Error on test \"%s\": expected rule Body Options\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.Body[0].jsonOptions) } switch terms := rule.Body[0].Terms.(type) { case []*Term: for _, term := range terms { if term.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected body Term JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, term.jsonOptions) + t.Fatalf("Error on test \"%s\": expected body Term Options\n%v\n, got\n%v", msg, *opt.JSONOptions, term.jsonOptions) } } case *SomeDecl: if terms.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected body Term JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) + t.Fatalf("Error on test \"%s\": expected body Term Options\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) } case *Every: if terms.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected body Term JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) + t.Fatalf("Error on test \"%s\": expected body Term Options\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) } case *Term: if terms.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected body Term JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) + t.Fatalf("Error on test \"%s\": expected body Term Options\n%v\n, got\n%v", msg, *opt.JSONOptions, terms.jsonOptions) } } if rule.jsonOptions != *opt.JSONOptions { - t.Fatalf("Error on test \"%s\": expected rule JSONOptions\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.jsonOptions) + t.Fatalf("Error on test \"%s\": expected rule Options\n%v\n, got\n%v", msg, *opt.JSONOptions, rule.jsonOptions) } } diff --git a/ast/policy.go b/ast/policy.go index 2822b82d87..fedc738ff2 100644 --- a/ast/policy.go +++ b/ast/policy.go @@ -12,6 +12,7 @@ import ( "strings" "time" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/util" ) @@ -153,7 +154,7 @@ type ( Text []byte Location *Location - jsonOptions JSONOptions + jsonOptions astJSON.Options } // Package represents the namespace of the documents produced @@ -162,7 +163,7 @@ type ( Path Ref `json:"path"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // Import represents a dependency on a document outside of the policy @@ -172,7 +173,7 @@ type ( Alias Var `json:"alias,omitempty"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // Rule represents a rule as defined in the language. Rules define the @@ -190,7 +191,7 @@ type ( // on the rule (e.g., printing, comparison, visiting, etc.) Module *Module `json:"-"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // Head represents the head of a rule. @@ -203,7 +204,7 @@ type ( Assign bool `json:"assign,omitempty"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // Args represents zero or more arguments to a rule. @@ -222,7 +223,7 @@ type ( Negated bool `json:"negated,omitempty"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // SomeDecl represents a variable declaration statement. The symbols are variables. @@ -230,7 +231,7 @@ type ( Symbols []*Term `json:"symbols"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } Every struct { @@ -240,7 +241,7 @@ type ( Body Body `json:"body"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } // With represents a modifier on an expression. @@ -249,7 +250,7 @@ type ( Value *Term `json:"value"` Location *Location `json:"location,omitempty"` - jsonOptions JSONOptions + jsonOptions astJSON.Options } ) @@ -428,10 +429,13 @@ func (c *Comment) Equal(other *Comment) bool { return c.Location.Equal(other.Location) && bytes.Equal(c.Text, other.Text) } -func (c *Comment) setJSONOptions(opts JSONOptions) { +func (c *Comment) setJSONOptions(opts astJSON.Options) { // Note: this is not used for location since Comments use default JSON marshaling // behavior with struct field names in JSON. c.jsonOptions = opts + if c.Location != nil { + c.Location.JSONOptions = opts + } } // Compare returns an integer indicating whether pkg is less than, equal to, @@ -478,8 +482,11 @@ func (pkg *Package) String() string { return fmt.Sprintf("package %v", path) } -func (pkg *Package) setJSONOptions(opts JSONOptions) { +func (pkg *Package) setJSONOptions(opts astJSON.Options) { pkg.jsonOptions = opts + if pkg.Location != nil { + pkg.Location.JSONOptions = opts + } } func (pkg *Package) MarshalJSON() ([]byte, error) { @@ -588,8 +595,11 @@ func (imp *Import) String() string { return strings.Join(buf, " ") } -func (imp *Import) setJSONOptions(opts JSONOptions) { +func (imp *Import) setJSONOptions(opts astJSON.Options) { imp.jsonOptions = opts + if imp.Location != nil { + imp.Location.JSONOptions = opts + } } func (imp *Import) MarshalJSON() ([]byte, error) { @@ -699,8 +709,11 @@ func (rule *Rule) String() string { return strings.Join(buf, " ") } -func (rule *Rule) setJSONOptions(opts JSONOptions) { +func (rule *Rule) setJSONOptions(opts astJSON.Options) { rule.jsonOptions = opts + if rule.Location != nil { + rule.Location.JSONOptions = opts + } } func (rule *Rule) MarshalJSON() ([]byte, error) { @@ -769,6 +782,17 @@ func NewHead(name Var, args ...*Term) *Head { return head } +// VarHead creates a head object, initializes its Name, Location, and Options, +// and returns the new head. +func VarHead(name Var, location *Location, jsonOpts *astJSON.Options) *Head { + h := NewHead(name) + h.Reference[0].Location = location + if jsonOpts != nil { + h.Reference[0].setJSONOptions(*jsonOpts) + } + return h +} + // RefHead returns a new Head object with the passed Ref. If args are provided, // the first will be used for the value. func RefHead(ref Ref, args ...*Term) *Head { @@ -915,8 +939,11 @@ func (head *Head) String() string { return buf.String() } -func (head *Head) setJSONOptions(opts JSONOptions) { +func (head *Head) setJSONOptions(opts astJSON.Options) { head.jsonOptions = opts + if head.Location != nil { + head.Location.JSONOptions = opts + } } func (head *Head) MarshalJSON() ([]byte, error) { @@ -1465,8 +1492,11 @@ func (expr *Expr) String() string { return strings.Join(buf, " ") } -func (expr *Expr) setJSONOptions(opts JSONOptions) { +func (expr *Expr) setJSONOptions(opts astJSON.Options) { expr.jsonOptions = opts + if expr.Location != nil { + expr.Location.JSONOptions = opts + } } func (expr *Expr) MarshalJSON() ([]byte, error) { @@ -1561,8 +1591,11 @@ func (d *SomeDecl) Hash() int { return termSliceHash(d.Symbols) } -func (d *SomeDecl) setJSONOptions(opts JSONOptions) { +func (d *SomeDecl) setJSONOptions(opts astJSON.Options) { d.jsonOptions = opts + if d.Location != nil { + d.Location.JSONOptions = opts + } } func (d *SomeDecl) MarshalJSON() ([]byte, error) { @@ -1635,8 +1668,11 @@ func (q *Every) KeyValueVars() VarSet { return vis.vars } -func (q *Every) setJSONOptions(opts JSONOptions) { +func (q *Every) setJSONOptions(opts astJSON.Options) { q.jsonOptions = opts + if q.Location != nil { + q.Location.JSONOptions = opts + } } func (q *Every) MarshalJSON() ([]byte, error) { @@ -1714,8 +1750,11 @@ func (w *With) SetLoc(loc *Location) { w.Location = loc } -func (w *With) setJSONOptions(opts JSONOptions) { +func (w *With) setJSONOptions(opts astJSON.Options) { w.jsonOptions = opts + if w.Location != nil { + w.Location.JSONOptions = opts + } } func (w *With) MarshalJSON() ([]byte, error) { diff --git a/ast/term.go b/ast/term.go index e89366aa74..9ef8ae7474 100644 --- a/ast/term.go +++ b/ast/term.go @@ -22,6 +22,7 @@ import ( "github.com/OneOfOne/xxhash" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/ast/location" "github.com/open-policy-agent/opa/util" ) @@ -294,7 +295,7 @@ type Term struct { Value Value `json:"value"` // the value of the Term as represented in Go Location *Location `json:"location,omitempty"` // the location of the Term in the source - jsonOptions JSONOptions + jsonOptions astJSON.Options } // NewTerm returns a new Term object. @@ -419,8 +420,11 @@ func (term *Term) IsGround() bool { return term.Value.IsGround() } -func (term *Term) setJSONOptions(opts JSONOptions) { +func (term *Term) setJSONOptions(opts astJSON.Options) { term.jsonOptions = opts + if term.Location != nil { + term.Location.JSONOptions = opts + } } // MarshalJSON returns the JSON encoding of the term. @@ -1028,6 +1032,20 @@ func (ref Ref) ConstantPrefix() Ref { return ref[:i] } +func (ref Ref) StringPrefix() Ref { + r := ref.Copy() + + for i := 1; i < len(ref); i++ { + switch r[i].Value.(type) { + case String: // pass + default: // cut off + return r[:i] + } + } + + return r +} + // GroundPrefix returns the ground portion of the ref starting from the head. By // definition, the head of the reference is always ground. func (ref Ref) GroundPrefix() Ref { diff --git a/bundle/bundle.go b/bundle/bundle.go index 5567a2ae59..66c8ce37c6 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/open-policy-agent/opa/ast" + astJSON "github.com/open-policy-agent/opa/ast/json" "github.com/open-policy-agent/opa/format" "github.com/open-policy-agent/opa/internal/file/archive" "github.com/open-policy-agent/opa/internal/merge" @@ -391,7 +392,7 @@ type Reader struct { verificationConfig *VerificationConfig skipVerify bool processAnnotations bool - jsonOptions *ast.JSONOptions + jsonOptions *astJSON.Options capabilities *ast.Capabilities files map[string]FileInfo // files in the bundle signature payload sizeLimitBytes int64 @@ -463,7 +464,7 @@ func (r *Reader) WithCapabilities(caps *ast.Capabilities) *Reader { } // WithJSONOptions sets the JSONOptions to use when parsing policy files -func (r *Reader) WithJSONOptions(opts *ast.JSONOptions) *Reader { +func (r *Reader) WithJSONOptions(opts *astJSON.Options) *Reader { r.jsonOptions = opts return r } diff --git a/cmd/eval_test.go b/cmd/eval_test.go index 8050fb614d..3e84f31701 100755 --- a/cmd/eval_test.go +++ b/cmd/eval_test.go @@ -1653,3 +1653,230 @@ func TestBundleWithStrictFlag(t *testing.T) { } } + +func TestIfElseIfElseNoBrace(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := 1 if false + else := 2`, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestIfElseIfElseBrace(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := 1 if { false } + else := 2`, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestIfElse(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := 1 `, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestElseNoIf(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else = x { + x=2 + } `, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestElseIf(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := x if { + x=2 + } `, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestElseIfElse(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := x if { + x=2 + 1==2 + } else =x { + x=3 + }`, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + defined, err := eval([]string{"data.bug.p"}, params, &buf) + if !defined || err != nil { + t.Fatalf("Unexpected undefined or error: %v", err) + } + }) +} + +func TestUnexpectedElseIfElseErr(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + p if false + else := x if { + x=2 + 1==2 + } else + x=3 + `, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + _, err := eval([]string{"data.bug.p"}, params, &buf) + + // Check if there was an error + if err == nil { + t.Fatalf("expected an error, but got nil") + } + + // Check the error message + errorMessage := err.Error() + expectedErrorMessage := "rego_parse_error: unexpected ident token: expected else value term or rule body" + if !strings.Contains(errorMessage, expectedErrorMessage) { + t.Fatalf("expected error message to contain '%s', but got '%s'", expectedErrorMessage, errorMessage) + } + }) +} + +func TestUnexpectedElseIfErr(t *testing.T) { + files := map[string]string{ + "bug.rego": `package bug + import future.keywords.if + q := 1 if false + else := 2 if + `, + } + + test.WithTempFS(files, func(path string) { + + params := newEvalCommandParams() + params.optimizationLevel = 1 + params.dataPaths = newrepeatedStringFlag([]string{path}) + params.entrypoints = newrepeatedStringFlag([]string{"bug/p"}) + + var buf bytes.Buffer + + _, err := eval([]string{"data.bug.p"}, params, &buf) + + // Check if there was an error + if err == nil { + t.Fatalf("expected an error, but got nil") + } + + // Check the error message + errorMessage := err.Error() + expectedErrorMessage := "rego_parse_error: unexpected eof token: rule body expected" + if !strings.Contains(errorMessage, expectedErrorMessage) { + t.Fatalf("expected error message to contain '%s', but got '%s'", expectedErrorMessage, errorMessage) + } + }) +} diff --git a/cmd/parse.go b/cmd/parse.go index ef529c8ea7..700794d550 100644 --- a/cmd/parse.go +++ b/cmd/parse.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/open-policy-agent/opa/ast" + astJSON "github.com/open-policy-agent/opa/ast/json" pr "github.com/open-policy-agent/opa/internal/presentation" "github.com/open-policy-agent/opa/loader" "github.com/open-policy-agent/opa/util" @@ -72,9 +73,10 @@ func parse(args []string, params *parseParams, stdout io.Writer, stderr io.Write parserOpts := ast.ParserOptions{ProcessAnnotation: true} if exposeLocation { - parserOpts.JSONOptions = &ast.JSONOptions{ - MarshalOptions: ast.JSONMarshalOptions{ - IncludeLocation: ast.NodeToggle{ + parserOpts.JSONOptions = &astJSON.Options{ + MarshalOptions: astJSON.MarshalOptions{ + IncludeLocationText: true, + IncludeLocation: astJSON.NodeToggle{ Term: true, Package: true, Comment: true, diff --git a/cmd/parse_test.go b/cmd/parse_test.go index a5f62009d5..b5371fbce4 100644 --- a/cmd/parse_test.go +++ b/cmd/parse_test.go @@ -129,9 +129,9 @@ func TestParseJSONOutputWithLocations(t *testing.T) { files := map[string]string{ "x.rego": `package x - - p = 1 - `, + +p = 1 +`, } errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{ format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), @@ -149,14 +149,16 @@ func TestParseJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 1 + "col": 1, + "text": "cGFja2FnZQ==" }, "path": [ { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 9 + "col": 9, + "text": "eA==" }, "type": "var", "value": "data" @@ -165,7 +167,8 @@ func TestParseJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 9 + "col": 9, + "text": "eA==" }, "type": "string", "value": "x" @@ -180,13 +183,15 @@ func TestParseJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 7 + "col": 5, + "text": "MQ==" }, "terms": { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 7 + "col": 5, + "text": "MQ==" }, "type": "boolean", "value": true @@ -199,13 +204,20 @@ func TestParseJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 7 + "col": 5, + "text": "MQ==" }, "type": "number", "value": 1 }, "ref": [ { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 1, + "text": "cA==" + }, "type": "var", "value": "p" } @@ -213,36 +225,276 @@ func TestParseJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 3 + "col": 1, + "text": "cCA9IDE=" } }, "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 3 + "col": 1, + "text": "cCA9IDE=" } } ] } `, "TEMPDIR", tempDirPath, -1) + gotLines := strings.Split(string(stdout), "\n") + wantLines := strings.Split(expectedOutput, "\n") + min := len(gotLines) + if len(wantLines) < min { + min = len(wantLines) + } + + for i := 0; i < min; i++ { + if gotLines[i] != wantLines[i] { + t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i]) + } + } + + if len(gotLines) != len(wantLines) { + t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines)) + } +} + +func TestParseRefsJSONOutput(t *testing.T) { + + files := map[string]string{ + "x.rego": `package x + + a.b.c := true + `, + } + errc, stdout, stderr, _ := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), + }) + if errc != 0 { + t.Fatalf("Expected exit code 0, got %v", errc) + } + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + + expectedOutput := `{ + "package": { + "path": [ + { + "type": "var", + "value": "data" + }, + { + "type": "string", + "value": "x" + } + ] + }, + "rules": [ + { + "body": [ + { + "index": 0, + "terms": { + "type": "boolean", + "value": true + } + } + ], + "head": { + "value": { + "type": "boolean", + "value": true + }, + "assign": true, + "ref": [ + { + "type": "var", + "value": "a" + }, + { + "type": "string", + "value": "b" + }, + { + "type": "string", + "value": "c" + } + ] + } + } + ] +} +` + if got, want := string(stdout), expectedOutput; got != want { t.Fatalf("Expected output\n%v\n, got\n%v", want, got) } } -func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { +func TestParseRefsJSONOutputWithLocations(t *testing.T) { files := map[string]string{ "x.rego": `package x - default allow = false - allow = true { - input.method == "GET" - input.path = ["getUser", user] - input.user == user +a.b.c := true +`, + } + errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), + jsonInclude: "locations", + }) + if errc != 0 { + t.Fatalf("Expected exit code 0, got %v", errc) + } + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + + expectedOutput := strings.Replace(`{ + "package": { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 1, + "text": "cGFja2FnZQ==" + }, + "path": [ + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 9, + "text": "eA==" + }, + "type": "var", + "value": "data" + }, + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 1, + "col": 9, + "text": "eA==" + }, + "type": "string", + "value": "x" + } + ] + }, + "rules": [ + { + "body": [ + { + "index": 0, + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 10, + "text": "dHJ1ZQ==" + }, + "terms": { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 10, + "text": "dHJ1ZQ==" + }, + "type": "boolean", + "value": true + } + } + ], + "head": { + "value": { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 10, + "text": "dHJ1ZQ==" + }, + "type": "boolean", + "value": true + }, + "assign": true, + "ref": [ + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 1, + "text": "YQ==" + }, + "type": "var", + "value": "a" + }, + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 3, + "text": "Yg==" + }, + "type": "string", + "value": "b" + }, + { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 5, + "text": "Yw==" + }, + "type": "string", + "value": "c" + } + ], + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 1, + "text": "YS5iLmMgOj0gdHJ1ZQ==" + } + }, + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 1, + "text": "YS5iLmMgOj0gdHJ1ZQ==" + } } - `, + ] +} +`, "TEMPDIR", tempDirPath, -1) + + gotLines := strings.Split(string(stdout), "\n") + wantLines := strings.Split(expectedOutput, "\n") + min := len(gotLines) + if len(wantLines) < min { + min = len(wantLines) + } + + for i := 0; i < min; i++ { + if gotLines[i] != wantLines[i] { + t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i]) + } + } + + if len(gotLines) != len(wantLines) { + t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines)) + } +} +func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { + + files := map[string]string{ + "x.rego": `package x + +default allow = false +allow = true { + input.method == "GET" + input.path = ["getUser", user] + input.user == user +} +`, } errc, stdout, stderr, tempDirPath := testParse(t, files, &parseParams{ format: util.NewEnumFlag(parseFormatJSON, []string{parseFormatPretty, parseFormatJSON}), @@ -260,14 +512,16 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 1 + "col": 1, + "text": "cGFja2FnZQ==" }, "path": [ { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 9 + "col": 9, + "text": "eA==" }, "type": "var", "value": "data" @@ -276,7 +530,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 1, - "col": 9 + "col": 9, + "text": "eA==" }, "type": "string", "value": "x" @@ -291,13 +546,15 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 3 + "col": 1, + "text": "ZGVmYXVsdA==" }, "terms": { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 3 + "col": 1, + "text": "ZGVmYXVsdA==" }, "type": "boolean", "value": true @@ -311,13 +568,20 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 19 + "col": 17, + "text": "ZmFsc2U=" }, "type": "boolean", "value": false }, "ref": [ { + "location": { + "file": "TEMPDIR/x.rego", + "row": 3, + "col": 9, + "text": "YWxsb3c=" + }, "type": "var", "value": "allow" } @@ -325,13 +589,15 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 11 + "col": 9, + "text": "YWxsb3cgPSBmYWxzZQ==" } }, "location": { "file": "TEMPDIR/x.rego", "row": 3, - "col": 3 + "col": 1, + "text": "ZGVmYXVsdA==" } }, { @@ -341,14 +607,16 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 7 + "col": 3, + "text": "aW5wdXQubWV0aG9kID09ICJHRVQi" }, "terms": [ { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 20 + "col": 16, + "text": "PT0=" }, "type": "ref", "value": [ @@ -356,7 +624,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 20 + "col": 16, + "text": "PT0=" }, "type": "var", "value": "equal" @@ -367,7 +636,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 7 + "col": 3, + "text": "aW5wdXQubWV0aG9k" }, "type": "ref", "value": [ @@ -375,7 +645,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 7 + "col": 3, + "text": "aW5wdXQ=" }, "type": "var", "value": "input" @@ -384,7 +655,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 13 + "col": 9, + "text": "bWV0aG9k" }, "type": "string", "value": "method" @@ -395,7 +667,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 5, - "col": 23 + "col": 19, + "text": "IkdFVCI=" }, "type": "string", "value": "GET" @@ -407,14 +680,16 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 7 + "col": 3, + "text": "aW5wdXQucGF0aCA9IFsiZ2V0VXNlciIsIHVzZXJd" }, "terms": [ { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 18 + "col": 14, + "text": "PQ==" }, "type": "ref", "value": [ @@ -422,7 +697,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 18 + "col": 14, + "text": "PQ==" }, "type": "var", "value": "eq" @@ -433,7 +709,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 7 + "col": 3, + "text": "aW5wdXQucGF0aA==" }, "type": "ref", "value": [ @@ -441,7 +718,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 7 + "col": 3, + "text": "aW5wdXQ=" }, "type": "var", "value": "input" @@ -450,7 +728,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 13 + "col": 9, + "text": "cGF0aA==" }, "type": "string", "value": "path" @@ -461,7 +740,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 20 + "col": 16, + "text": "WyJnZXRVc2VyIiwgdXNlcl0=" }, "type": "array", "value": [ @@ -469,7 +749,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 21 + "col": 17, + "text": "ImdldFVzZXIi" }, "type": "string", "value": "getUser" @@ -478,7 +759,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 6, - "col": 32 + "col": 28, + "text": "dXNlcg==" }, "type": "var", "value": "user" @@ -492,14 +774,16 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 7 + "col": 3, + "text": "aW5wdXQudXNlciA9PSB1c2Vy" }, "terms": [ { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 18 + "col": 14, + "text": "PT0=" }, "type": "ref", "value": [ @@ -507,7 +791,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 18 + "col": 14, + "text": "PT0=" }, "type": "var", "value": "equal" @@ -518,7 +803,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 7 + "col": 3, + "text": "aW5wdXQudXNlcg==" }, "type": "ref", "value": [ @@ -526,7 +812,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 7 + "col": 3, + "text": "aW5wdXQ=" }, "type": "var", "value": "input" @@ -535,7 +822,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 13 + "col": 9, + "text": "dXNlcg==" }, "type": "string", "value": "user" @@ -546,7 +834,8 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 7, - "col": 21 + "col": 17, + "text": "dXNlcg==" }, "type": "var", "value": "user" @@ -560,13 +849,20 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 4, - "col": 13 + "col": 9, + "text": "dHJ1ZQ==" }, "type": "boolean", "value": true }, "ref": [ { + "location": { + "file": "TEMPDIR/x.rego", + "row": 4, + "col": 1, + "text": "YWxsb3c=" + }, "type": "var", "value": "allow" } @@ -574,21 +870,36 @@ func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) { "location": { "file": "TEMPDIR/x.rego", "row": 4, - "col": 5 + "col": 1, + "text": "YWxsb3cgPSB0cnVl" } }, "location": { "file": "TEMPDIR/x.rego", "row": 4, - "col": 5 + "col": 1, + "text": "YWxsb3cgPSB0cnVlIHsKICBpbnB1dC5tZXRob2QgPT0gIkdFVCIKICBpbnB1dC5wYXRoID0gWyJnZXRVc2VyIiwgdXNlcl0KICBpbnB1dC51c2VyID09IHVzZXIKfQ==" } } ] } `, "TEMPDIR", tempDirPath, -1) - if got, want := string(stdout), expectedOutput; got != want { - t.Fatalf("Expected output\n%v\n, got\n%v", want, got) + gotLines := strings.Split(string(stdout), "\n") + wantLines := strings.Split(expectedOutput, "\n") + min := len(gotLines) + if len(wantLines) < min { + min = len(wantLines) + } + + for i := 0; i < min; i++ { + if gotLines[i] != wantLines[i] { + t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i]) + } + } + + if len(gotLines) != len(wantLines) { + t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines)) } } diff --git a/compile/compile_bench_test.go b/compile/compile_bench_test.go index a26773e98f..079f4d41ad 100644 --- a/compile/compile_bench_test.go +++ b/compile/compile_bench_test.go @@ -3,6 +3,7 @@ package compile import ( "context" "fmt" + "io/fs" "strings" "testing" @@ -22,11 +23,11 @@ func BenchmarkCompileDynamicPolicy(b *testing.B) { for _, n := range numPolicies { testcase := generateDynamicPolicyBenchmarkData(n) - b.Run(fmt.Sprintf("%d", n), func(b *testing.B) { - test.WithTempFS(testcase, func(root string) { - b.ResetTimer() - + test.WithTestFS(testcase, true, func(root string, fileSys fs.FS) { + b.ResetTimer() + b.Run(fmt.Sprintf("%d", n), func(b *testing.B) { compiler := New(). + WithFS(fileSys). WithPaths(root) err := compiler.Build(context.Background()) diff --git a/docs/content/_index.md b/docs/content/_index.md index 6cce6e1a7f..362d7c5bef 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -60,7 +60,7 @@ There are three kinds of components in the system: * Networks connect servers and can be public or private. Public networks are connected to the Internet. * Ports attach servers to networks. -All of the servers, networks, and ports are provisioned by a script. The script +All the servers, networks, and ports are provisioned by a script. The script receives a JSON representation of the system as input: ```live:example:input @@ -724,6 +724,11 @@ shell_accessible ```live:example/logical_or/partial_set:output ``` +{{< info >}} +💡 there's a [blog post](https://www.styra.com/blog/how-to-express-or-in-rego/) that goes into much more detail +on this topic showing different methods express OR in idiomatic Rego for different use cases. +{{< /info >}} + ### Putting It Together diff --git a/docs/website/content/integrations/enterprise-contract.md b/docs/website/content/integrations/enterprise-contract.md new file mode 100644 index 0000000000..804d1deb59 --- /dev/null +++ b/docs/website/content/integrations/enterprise-contract.md @@ -0,0 +1,22 @@ +--- +title: Enterprise Contract +software: +- enterprise-contract +inventors: +- enterprise-contract +labels: + category: security + layer: application +code: +- https://github.com/enterprise-contract/ec-cli +blogs: +- https://enterprisecontract.dev/posts/ +docs_features: + go-integration: + note: | + The [Enterprise Contract](https://enterprisecontract.dev/) uses the OPA go + library to process rego policies when validating the signatures and + attestations of container images and other software artifacts. +--- +Securely verify supply chain artifacts, and enforce policies about how they were built and tested, +in a manageable, scalable, and declarative way. diff --git a/docs/website/content/integrations/expressing-or.md b/docs/website/content/integrations/expressing-or.md new file mode 100644 index 0000000000..edd8b1b2a3 --- /dev/null +++ b/docs/website/content/integrations/expressing-or.md @@ -0,0 +1,19 @@ +--- +title: Express OR in Rego +subtitle: Idiomatic Rego Examples +labels: + category: learning + layer: web +inventors: +- styra +blogs: +- http://styra.com/blog/how-to-express-or-in-rego +docs_features: + learning-rego: + note: | + This [learning material](http://styra.com/blog/how-to-express-or-in-rego) + is a great way to learn how to migrate logic from other languages to Rego. +--- + +This [learning material](http://styra.com/blog/how-to-express-or-in-rego) +is a great way to learn how to migrate logic from other languages to Rego. diff --git a/docs/website/content/integrations/regocpp.md b/docs/website/content/integrations/regocpp.md new file mode 100644 index 0000000000..9fcf9c5ef8 --- /dev/null +++ b/docs/website/content/integrations/regocpp.md @@ -0,0 +1,13 @@ +--- +title: regocpp +software: +- regopy +- regorust +inventors: +- microsoft +tutorials: +- https://microsoft.github.io/rego-cpp/ +code: +- https://github.com/microsoft/rego-cpp +--- +The rego-cpp project is a Rego compiler and runtime implemented in C++. As such, it provides the ability to integrate Rego natively into a wider range of languages. We currrently support C, C++, Rust, and Python, and are largely compatible with v0.55.0 of the language. \ No newline at end of file diff --git a/docs/website/content/organizations/enterprise-contract.md b/docs/website/content/organizations/enterprise-contract.md new file mode 100644 index 0000000000..684bde7b93 --- /dev/null +++ b/docs/website/content/organizations/enterprise-contract.md @@ -0,0 +1,4 @@ +--- +link: https://enterprisecontract.dev/ +title: Enterprise Contract +--- diff --git a/docs/website/content/softwares/enterprise-contract.md b/docs/website/content/softwares/enterprise-contract.md new file mode 100644 index 0000000000..0fce1f4ed9 --- /dev/null +++ b/docs/website/content/softwares/enterprise-contract.md @@ -0,0 +1,4 @@ +--- +link: https://github.com/enterprise-contract/ +title: Enterprise Contract +--- diff --git a/docs/website/content/softwares/regopy.md b/docs/website/content/softwares/regopy.md new file mode 100644 index 0000000000..17c2f9168b --- /dev/null +++ b/docs/website/content/softwares/regopy.md @@ -0,0 +1,4 @@ +--- +link: https://pypi.org/project/regopy/ +title: regopy +--- diff --git a/docs/website/content/softwares/regorust.md b/docs/website/content/softwares/regorust.md new file mode 100644 index 0000000000..72e4b6a0db --- /dev/null +++ b/docs/website/content/softwares/regorust.md @@ -0,0 +1,4 @@ +--- +link: https://crates.io/crates/regorust +title: regorust +--- diff --git a/docs/website/layouts/support/section.html.html b/docs/website/layouts/support/section.html.html index d7d87e3b19..f3b24b1e8a 100644 --- a/docs/website/layouts/support/section.html.html +++ b/docs/website/layouts/support/section.html.html @@ -17,27 +17,31 @@