mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
cmd/check: report wrapped structured errors individually (#8912)
Fixes #3663 In JSON mode, `opa check -b` collapsed all compilation errors into one opaque string, unlike non-bundle mode which lists each with its code and location. The bundle loader wraps errors as `fmt.Errorf("bundle %s: %w", ...)`, and NewOutputErrors default case stringified the wrapper instead of the structured ast.Errors inside it. The default case now unwraps and recurses, keeping the wrapper's message only when unwrapping reveals nothing structured. Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -192,6 +193,17 @@ func NewOutputErrors(err error) []OutputError {
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Unwrap wrapped errors (e.g. the bundle loader's
|
||||
// fmt.Errorf("bundle %s: %w", ...)) to report the structured errors
|
||||
// they hide individually rather than as one opaque string (#3663).
|
||||
// Keep the wrapper's message if unwrapping reveals nothing structured.
|
||||
hasStructuredCode := func(e OutputError) bool { return e.Code != "" }
|
||||
if inner := errors.Unwrap(err); inner != nil {
|
||||
if unwrapped := NewOutputErrors(inner); slices.ContainsFunc(unwrapped, hasStructuredCode) {
|
||||
return unwrapped
|
||||
}
|
||||
}
|
||||
|
||||
// Any errors which don't have a structure we know about
|
||||
// are converted to their string representation only.
|
||||
errs = []OutputError{{
|
||||
|
||||
@@ -123,6 +123,50 @@ func TestOutputJSONErrorStructuredASTErr(t *testing.T) {
|
||||
validateJSONOutput(t, err, expected)
|
||||
}
|
||||
|
||||
func TestOutputJSONErrorWrapped(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
err error
|
||||
expected string
|
||||
}{
|
||||
"structured errors are unwrapped and listed individually": {
|
||||
err: fmt.Errorf("bundle /some/path: %w", ast.Errors{
|
||||
&ast.Error{Code: "rego_parse_error", Message: "first"},
|
||||
&ast.Error{Code: "rego_parse_error", Message: "second"},
|
||||
}),
|
||||
expected: `{
|
||||
"errors": [
|
||||
{
|
||||
"message": "first",
|
||||
"code": "rego_parse_error"
|
||||
},
|
||||
{
|
||||
"message": "second",
|
||||
"code": "rego_parse_error"
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
"plain error keeps the wrapper's message": {
|
||||
err: fmt.Errorf("failed to load config: %w", errors.New("file not found")),
|
||||
expected: `{
|
||||
"errors": [
|
||||
{
|
||||
"message": "failed to load config: file not found"
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
validateJSONOutput(t, tc.err, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputJSONErrorStructuredStorageErr(t *testing.T) {
|
||||
store := inmem.New()
|
||||
txn := storage.NewTransactionOrDie(t.Context(), store)
|
||||
|
||||
Reference in New Issue
Block a user