topdown/rego: Add BuiltinErrorList support to rego package, add to eval command (#5487)

```
$ cat pol.rego
package play

this_errors(number) := result {
        result := number / 0
}

this_errors_too(number) := result {
        result := number / 0
}

res1 := this_errors(1)

res2 := this_errors_too(1)

$ go run main.go eval --show-builtin-errors -d pol.rego data.play
{
  "errors": [
    {
      "message": "div: divide by zero",
      "code": "eval_builtin_error",
      "location": {
        "file": "pol.rego",
        "row": 4,
        "col": 12
      }
    },
    {
      "message": "div: divide by zero",
      "code": "eval_builtin_error",
      "location": {
        "file": "pol.rego",
        "row": 8,
        "col": 12
      }
    }
  ],
  "result": [
    {
      "expressions": [
        {
          "value": {},
          "text": "data.play",
          "location": {
            "row": 1,
            "col": 1
          }
        }
      ]
    }
  ]
}
```

Signed-off-by: Charlie Egan <charlieegan3@users.noreply.github.com>
This commit is contained in:
Charlie Egan
2023-01-03 13:25:49 +00:00
committed by GitHub
parent 36fdcef93f
commit ac64e78c69
5 changed files with 117 additions and 21 deletions
+42 -16
View File
@@ -44,6 +44,7 @@ type evalCommandParams struct {
disableIndexing bool
disableEarlyExit bool
strictBuiltinErrors bool
showBuiltinErrors bool
dataPaths repeatedStringFlag
inputPath string
imports repeatedStringFlag
@@ -289,7 +290,8 @@ access.
evalCommand.Flags().BoolVarP(&params.shallowInlining, "shallow-inlining", "", false, "disable inlining of rules that depend on unknowns")
evalCommand.Flags().BoolVar(&params.disableIndexing, "disable-indexing", false, "disable indexing optimizations")
evalCommand.Flags().BoolVar(&params.disableEarlyExit, "disable-early-exit", false, "disable 'early exit' optimizations")
evalCommand.Flags().BoolVarP(&params.strictBuiltinErrors, "strict-builtin-errors", "", false, "treat built-in function errors as fatal")
evalCommand.Flags().BoolVarP(&params.strictBuiltinErrors, "strict-builtin-errors", "", false, "treat the first built-in function error encountered as fatal")
evalCommand.Flags().BoolVarP(&params.showBuiltinErrors, "show-builtin-errors", "", false, "collect and return all encountered built-in errors, built in errors are not fatal")
evalCommand.Flags().BoolVarP(&params.instrument, "instrument", "", false, "enable query instrumentation metrics (implies --metrics)")
evalCommand.Flags().BoolVarP(&params.profile, "profile", "", false, "perform expression profiling")
evalCommand.Flags().VarP(&params.profileCriteria, "profile-sort", "", "set sort order of expression profiler results")
@@ -376,6 +378,11 @@ func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
result.AggregatedMetrics = timersAggregated
}
var builtInErrorCount int
if ectx.params.showBuiltinErrors {
builtInErrorCount = len(*(ectx.builtInErrorList))
}
switch ectx.params.outputFormat.String() {
case evalBindingsOutput:
err = pr.Bindings(w, result)
@@ -393,7 +400,11 @@ func eval(args []string, params evalCommandParams, w io.Writer) (bool, error) {
if err != nil {
return false, err
} else if len(result.Errors) > 0 {
} else if errorCount := len(result.Errors); errorCount > 0 && errorCount != builtInErrorCount {
// if we only have built-in errors, we don't want to return an error. If
// strict-builtin-errors is set the first built-in error will be returned
// in a result error instead.
// If the rego package returned an error, return a special error here so
// that the command doesn't print the same error twice. The error will
// have been printed above by the presentation package.
@@ -436,6 +447,11 @@ func evalOnce(ctx context.Context, ectx *evalContext) pr.Output {
}
result.Errors = pr.NewOutputErrors(resultErr)
if ectx.builtInErrorList != nil {
for _, err := range *(ectx.builtInErrorList) {
result.Errors = append(result.Errors, pr.NewOutputErrors(&err)...)
}
}
if ectx.params.explain != nil {
switch ectx.params.explain.String() {
@@ -473,13 +489,14 @@ func evalOnce(ctx context.Context, ectx *evalContext) pr.Output {
}
type evalContext struct {
params evalCommandParams
metrics metrics.Metrics
profiler *resettableProfiler
cover *cover.Cover
tracer *topdown.BufferTracer
regoArgs []func(*rego.Rego)
evalArgs []rego.EvalOption
params evalCommandParams
metrics metrics.Metrics
profiler *resettableProfiler
cover *cover.Cover
tracer *topdown.BufferTracer
regoArgs []func(*rego.Rego)
evalArgs []rego.EvalOption
builtInErrorList *[]topdown.Error
}
func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
@@ -622,6 +639,14 @@ func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
if params.strictBuiltinErrors {
regoArgs = append(regoArgs, rego.StrictBuiltinErrors(true))
if params.showBuiltinErrors {
return nil, fmt.Errorf("cannot use --show-builtin-errors with --strict-builtin-errors, --strict-builtin-errors will return the first built-in error encountered immediately")
}
}
var builtInErrors []topdown.Error
if params.showBuiltinErrors {
regoArgs = append(regoArgs, rego.BuiltinErrorList(&builtInErrors))
}
if params.capabilities != nil {
@@ -633,13 +658,14 @@ func setupEval(args []string, params evalCommandParams) (*evalContext, error) {
}
evalCtx := &evalContext{
params: params,
metrics: m,
profiler: &rp,
cover: c,
tracer: tracer,
regoArgs: regoArgs,
evalArgs: evalArgs,
params: params,
metrics: m,
profiler: &rp,
cover: c,
tracer: tracer,
regoArgs: regoArgs,
evalArgs: evalArgs,
builtInErrorList: &builtInErrors,
}
return evalCtx, nil
+2 -1
View File
@@ -547,10 +547,11 @@ opa eval <query> [flags]
--profile-sort string set sort order of expression profiler results
-s, --schema string set schema file path or directory path
--shallow-inlining disable inlining of rules that depend on unknowns
--show-builtin-errors collect and return all encountered built-in errors, built in errors are not fatal
--stdin read query from stdin
-I, --stdin-input read input document from stdin
-S, --strict enable compiler strict mode
--strict-builtin-errors treat built-in function errors as fatal
--strict-builtin-errors treat the first built-in function error encountered as fatal
-t, --target {rego,wasm} set the runtime to exercise (default rego)
--timeout duration set eval timeout (default unlimited)
-u, --unknowns stringArray set paths to treat as unknown during partial evaluation (default [input])
+9
View File
@@ -528,6 +528,7 @@ type Rego struct {
interQueryBuiltinCache cache.InterQueryCache
ndBuiltinCache builtins.NDBCache
strictBuiltinErrors bool
builtinErrorList *[]topdown.Error
resolvers []refResolver
schemaSet *ast.SchemaSet
target string // target type (wasm, rego, etc.)
@@ -1055,6 +1056,13 @@ func StrictBuiltinErrors(yes bool) func(r *Rego) {
}
}
// BuiltinErrorList supplies an error slice to store built-in function errors.
func BuiltinErrorList(list *[]topdown.Error) func(r *Rego) {
return func(r *Rego) {
r.builtinErrorList = list
}
}
// Resolver sets a Resolver for a specified ref path.
func Resolver(ref ast.Ref, r resolver.Resolver) func(r *Rego) {
return func(rego *Rego) {
@@ -1959,6 +1967,7 @@ func (r *Rego) eval(ctx context.Context, ectx *EvalContext) (ResultSet, error) {
WithEarlyExit(ectx.earlyExit).
WithInterQueryBuiltinCache(ectx.interQueryBuiltinCache).
WithStrictBuiltinErrors(r.strictBuiltinErrors).
WithBuiltinErrorList(r.builtinErrorList).
WithSeed(ectx.seed).
WithPrintHook(ectx.printHook).
WithDistributedTracingOpts(r.distributedTacingOpts)
+17
View File
@@ -2253,6 +2253,23 @@ func TestStrictBuiltinErrors(t *testing.T) {
}
}
func TestBuiltinErrorList(t *testing.T) {
var buf []topdown.Error
_, err := New(Query("1/0"), BuiltinErrorList(&buf)).Eval(context.Background())
if err != nil {
t.Fatal("unexpected error")
}
if len(buf) != 1 {
t.Fatal("expected 1 error in buffer")
}
if buf[0].Error() != "1/0: eval_builtin_error: div: divide by zero" {
t.Fatal("expected divide by zero error but got:", buf[0].Error())
}
}
func TestTimeSeedingOptions(t *testing.T) {
ctx := context.Background()
+47 -4
View File
@@ -54,6 +54,7 @@ type Query struct {
interQueryBuiltinCache cache.InterQueryCache
ndBuiltinCache builtins.NDBCache
strictBuiltinErrors bool
builtinErrorList *[]Error
strictObjects bool
printHook print.Hook
tracingOpts tracing.Options
@@ -256,6 +257,14 @@ func (q *Query) WithStrictBuiltinErrors(yes bool) *Query {
return q
}
// WithBuiltinErrorList supplies a pointer to an Error slice to store built-in function errors
// encountered during evaluation. This error slice can be inspected after evaluation to determine
// which built-in function errors occurred.
func (q *Query) WithBuiltinErrorList(list *[]Error) *Query {
q.builtinErrorList = list
return q
}
// WithResolver configures an external resolver to use for the given ref.
func (q *Query) WithResolver(ref ast.Ref, r resolver.Resolver) *Query {
q.external.Put(ref, r)
@@ -419,8 +428,25 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support []
support = e.saveSupport.List()
if q.strictBuiltinErrors && len(e.builtinErrors.errs) > 0 {
err = e.builtinErrors.errs[0]
if len(e.builtinErrors.errs) > 0 {
if q.strictBuiltinErrors {
err = e.builtinErrors.errs[0]
} else if q.builtinErrorList != nil {
// If a builtinErrorList has been supplied, we must use pointer indirection
// to append to it. builtinErrorList is a slice pointer so that errors can be
// appended to it without returning a new slice and changing the interface
// of PartialRun.
for _, err := range e.builtinErrors.errs {
if tdError, ok := err.(*Error); ok {
*(q.builtinErrorList) = append(*(q.builtinErrorList), *tdError)
} else {
*(q.builtinErrorList) = append(*(q.builtinErrorList), Error{
Code: BuiltinErr,
Message: err.Error(),
})
}
}
}
}
for i := range support {
@@ -504,8 +530,25 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error {
return iter(qr)
})
if q.strictBuiltinErrors && err == nil && len(e.builtinErrors.errs) > 0 {
err = e.builtinErrors.errs[0]
if len(e.builtinErrors.errs) > 0 {
if q.strictBuiltinErrors {
err = e.builtinErrors.errs[0]
} else if q.builtinErrorList != nil {
// If a builtinErrorList has been supplied, we must use pointer indirection
// to append to it. builtinErrorList is a slice pointer so that errors can be
// appended to it without returning a new slice and changing the interface
// of Iter.
for _, err := range e.builtinErrors.errs {
if tdError, ok := err.(*Error); ok {
*(q.builtinErrorList) = append(*(q.builtinErrorList), *tdError)
} else {
*(q.builtinErrorList) = append(*(q.builtinErrorList), Error{
Code: BuiltinErr,
Message: err.Error(),
})
}
}
}
}
q.metrics.Timer(metrics.RegoQueryEval).Stop()