Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for multiple errors per file. #357

Merged
merged 23 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
90 changes: 77 additions & 13 deletions errors/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ const separator = ": "
// The kind of error (eg.: HCLSyntax, TerramateSchema, etc).
// hcl.Range
// The file range where the error originated.
// errors.StackMeta
// errors.StackMeta
// The stack that originated the error.
// *List
// The underlying error list. By wrapping an error list, you also wrap all
i4ki marked this conversation as resolved.
Show resolved Hide resolved
// of its error items.
// error
// The underlying error that triggered this one.
// hcl.Diagnostics
Expand Down Expand Up @@ -169,6 +172,39 @@ func E(args ...interface{}) *Error {
if e.isEmpty() {
panic(errors.New("empty error"))
}

errs, ok := e.Err.(*List)
if ok {
// if the underlying error is a *List we wrap all of its elements so they
// carry all the context needed to print them individually.
// Eg.:
// errs := errors.L()
// obj, err := something.Do()
// if err != nil {
// errs.Append(errors.E(ErrSomethingBadHappened, err))
// }
//
// if `err` is an *errors.List, the code above means that all of their
// error items have the kind `ErrSomethingBadHappened`.
//

// code below captures all arguments but the *List so we can wrap the
// elements of the list with same semantics intended by the caller of E.
wrappingArgs := []interface{}{}
for _, arg := range args {
_, ok := arg.(*List)
if !ok {
wrappingArgs = append(wrappingArgs, arg)
}
}

for i, el := range errs.errs {
errs.errs[i] = E(append(wrappingArgs, el)...)
}

return e
}

prev, ok := e.Err.(*Error)
if !ok {
return e
Expand Down Expand Up @@ -206,15 +242,9 @@ func (e *Error) isEmpty() bool {
return e.FileRange == hcl.Range{} && e.Kind == "" && e.Description == "" && e.Stack == nil
}

func (e *Error) error(verbose bool) string {
func (e *Error) error(members []interface{}, verbose bool) string {
i4ki marked this conversation as resolved.
Show resolved Hide resolved
var errParts []string
for _, arg := range []interface{}{
e.FileRange,
e.Kind,
e.Description,
e.Stack,
e.Err,
} {
for _, arg := range members {
emptyRange := hcl.Range{}
switch v := arg.(type) {
case hcl.Range:
Expand Down Expand Up @@ -253,8 +283,9 @@ func (e *Error) error(verbose bool) string {
case error:
if v != nil {
errmsg := ""
if e, ok := v.(interface{ Detailed() string }); ok && verbose {
errmsg = e.Detailed()
e, ok := v.(*Error)
if ok {
errmsg = e.error(e.defaultErrorParts(), verbose)
} else {
errmsg = v.Error()
}
Expand All @@ -270,14 +301,47 @@ func (e *Error) error(verbose bool) string {
return strings.Join(errParts, separator)
}

func (e *Error) defaultErrorParts() []interface{} {
i4ki marked this conversation as resolved.
Show resolved Hide resolved
return []interface{}{
e.FileRange,
e.Kind,
e.Description,
e.Stack,
e.Err,
}
}

// Error returns the error message.
func (e *Error) Error() string {
return e.error(false)
return e.error(e.defaultErrorParts(), false)
}

// Detailed returns a detailed error message.
func (e *Error) Detailed() string {
return e.error(true)
return e.error(e.defaultErrorParts(), true)
}

// AsList returns the error as a list.
// If it's underlying error is a *List, then it just returns it because
// they're already explicitly wrapped.
func (e *Error) AsList() *List {
if errs, ok := e.Err.(*List); ok {
i4ki marked this conversation as resolved.
Show resolved Hide resolved
return errs
}

return L(e)
}

// Message returns the error message without some metadata.
// This method is suitable for editor extensions that needs to handle the
// metadata themselves.
func (e *Error) Message() string {
katcipis marked this conversation as resolved.
Show resolved Hide resolved
return e.error([]interface{}{
e.Kind,
e.Description,
e.Stack,
e.Err,
}, false)
}

// Is tells if err matches the target error.
Expand Down
62 changes: 42 additions & 20 deletions errors/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,21 @@ func (l *List) Error() string {
return fmt.Sprintf("%s (and %d elided errors)", errmsg, len(l.errs)-1)
}

// Errors returns all errors contained on the list that are of the type Error
// or that have an error of type Error wrapped inside them.
// Any other errors will be ignored.
func (l *List) Errors() []*Error {
var errs []*Error
// Errors returns all errors contained on the list.
// It flattens out the wrapped error lists inside *error.Error.
func (l *List) Errors() []error {
var errs []error
for _, err := range l.errs {
var e *Error
if errors.As(err, &e) {
switch e := err.(type) {
case *Error:
if el, ok := e.Err.(*List); ok {
i4ki marked this conversation as resolved.
Show resolved Hide resolved
errs = append(errs, el.Errors()...)
} else {
errs = append(errs, e)
}
case *List:
errs = append(errs, e.Errors()...)
default:
errs = append(errs, e)
}
}
Expand Down Expand Up @@ -104,23 +111,38 @@ func (l *List) Detailed() string {
//
// Any error of type errors.List will be flattened inside
// the error list.
func (l *List) Append(err error) {
if err == nil {
func (l *List) Append(errs ...error) {
katcipis marked this conversation as resolved.
Show resolved Hide resolved
if len(errs) == 0 {
return
}
switch e := err.(type) {
case hcl.Diagnostics:
{
for _, diag := range e {
l.errs = append(l.errs, E(diag))
}

for _, err := range errs {
if err == nil {
continue
}
case *List:
{
l.errs = append(l.errs, e.errs...)

switch e := err.(type) {
case hcl.Diagnostics:
{
katcipis marked this conversation as resolved.
Show resolved Hide resolved
for _, diag := range e {
l.errs = append(l.errs, E(diag))
}
}
case *List:
{
for _, err := range e.errs {
l.Append(err)
}
}
case *Error:
if el, ok := e.Err.(*List); ok {
l.errs = append(l.errs, el.errs...)
} else {
l.errs = append(l.errs, e)
}
default:
l.errs = append(l.errs, err)
}
default:
l.errs = append(l.errs, err)
}
}

Expand Down
33 changes: 16 additions & 17 deletions errors/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,26 @@ func TestEmptyErrorListReturnsEmptyErrors(t *testing.T) {
}

func TestErrorListReturnsAllErrors(t *testing.T) {
// This test was updated to reflect the changed API of errors.List.Errors().
// Now it doesn't ignore errors anymore.

i4ki marked this conversation as resolved.
Show resolved Hide resolved
e := errors.L()

assert.EqualInts(t, 0, len(e.Errors()))

notIgnored := stderrors.New("ignored")
i4ki marked this conversation as resolved.
Show resolved Hide resolved
e.Append(E("one"))
e.Append(stdfmt.Errorf("wrapped: %w", E("two")))
e.Append(stderrors.New("ignored"))
e.Append(notIgnored)
e.Append(E("three"))

errs := e.Errors()

assert.EqualInts(t, 3, len(errs))
assert.EqualInts(t, 4, len(errs))
assert.IsError(t, errs[0], E("one"))
assert.IsError(t, errs[1], E("two"))
assert.IsError(t, errs[2], E("three"))
assert.IsError(t, errs[2], notIgnored)
assert.IsError(t, errs[3], E("three"))
}

func TestEmptyErrorListStringRepresentationIsEmpty(t *testing.T) {
Expand Down Expand Up @@ -79,13 +84,13 @@ func TestErrorListFlattensAllDiagnostics(t *testing.T) {
detail2 = "error 2"
)
var (
range1 = &hcl.Range{
range1 = hcl.Range{
Filename: "file1.tm",
Start: hcl.Pos{Line: 1, Column: 5, Byte: 3},
End: hcl.Pos{Line: 1, Column: 10, Byte: 13},
}

range2 = &hcl.Range{
range2 = hcl.Range{
Filename: "file2.tm",
Start: hcl.Pos{Line: 2, Column: 6, Byte: 4},
End: hcl.Pos{Line: 2, Column: 11, Byte: 14},
Expand All @@ -95,27 +100,21 @@ func TestErrorListFlattensAllDiagnostics(t *testing.T) {
&hcl.Diagnostic{
Detail: detail1,
Severity: hcl.DiagError,
Subject: range1,
Subject: &range1,
},
&hcl.Diagnostic{
Detail: detail2,
Severity: hcl.DiagError,
Subject: range2,
Subject: &range2,
},
}

errs := errors.L()
errs.Append(diags)

wantErrs := []*errors.Error{
{
Description: detail1,
FileRange: *range1,
},
{
Description: detail2,
FileRange: *range2,
},
wantErrs := []error{
i4ki marked this conversation as resolved.
Show resolved Hide resolved
errors.E(detail1, range1),
errors.E(detail2, range2),
}
gotErrs := errs.Errors()

Expand All @@ -138,7 +137,7 @@ func TestErrorListFlattensOtherErrorList(t *testing.T) {
errs := errors.L(error1)
errs.Append(errors.L(error2, error3))

wantErrs := []*errors.Error{error1, error2, error3}
wantErrs := []error{error1, error2, error3}
gotErrs := errs.Errors()

if diff := cmp.Diff(gotErrs, wantErrs); diff != "" {
Expand Down