mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-30 03:32:59 +03:00
a3cd01f36a
* Diff refactor
364 lines
8.6 KiB
Go
364 lines
8.6 KiB
Go
// Package errz is sq's error package. It annotates errors with stack traces,
|
|
// and provides functionality for working with multiple errors, and error
|
|
// chains.
|
|
//
|
|
// This package is the lovechild of Dave Cheney's pkg/errors and
|
|
// Uber's go.uber.org/multierr, and much of the code is borrowed
|
|
// from those packages.
|
|
package errz
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// Opt is a functional option. Use with Err or New.
|
|
type Opt interface {
|
|
apply(*errz)
|
|
}
|
|
|
|
// Err annotates err with a stack trace at the point Err was called.
|
|
// It is equivalent to Wrap(err, ""). If err is nil, Err returns nil.
|
|
func Err(err error, opts ...Opt) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
ez := &errz{stack: callers(0), error: err}
|
|
for _, opt := range opts {
|
|
opt.apply(ez)
|
|
}
|
|
return ez
|
|
}
|
|
|
|
// New returns an error with the supplied message, recording the
|
|
// stack trace at the point it was called.
|
|
func New(message string, opts ...Opt) error {
|
|
ez := &errz{stack: callers(0), msg: message}
|
|
for _, opt := range opts {
|
|
opt.apply(ez)
|
|
}
|
|
return ez
|
|
}
|
|
|
|
// Errorf works like fmt.Errorf, but it also records the stack trace
|
|
// at the point it was called. If the format string includes the %w verb,
|
|
// fmt.Errorf is first called to construct the error, and then the
|
|
// returned error is again wrapped to record the stack trace.
|
|
func Errorf(format string, args ...any) error {
|
|
if strings.Contains(format, "%w") {
|
|
return &errz{stack: callers(0), error: fmt.Errorf(format, args...)}
|
|
}
|
|
return &errz{stack: callers(0), msg: fmt.Sprintf(format, args...)}
|
|
}
|
|
|
|
// errz is our error type that does the magic.
|
|
type errz struct {
|
|
error
|
|
*stack
|
|
msg string
|
|
}
|
|
|
|
// inner implements stackTracer.
|
|
func (e *errz) inner() error { return e.error }
|
|
|
|
// stackTrace implements stackTracer.
|
|
func (e *errz) stackTrace() *StackTrace {
|
|
if e == nil || e.stack == nil {
|
|
return nil
|
|
}
|
|
|
|
st := e.stack.stackTrace()
|
|
if st != nil {
|
|
st.Error = e
|
|
}
|
|
return st
|
|
}
|
|
|
|
// Error implements stdlib error interface.
|
|
func (e *errz) Error() string {
|
|
if e.msg == "" {
|
|
if e.error == nil {
|
|
return ""
|
|
}
|
|
return e.error.Error()
|
|
}
|
|
if e.error == nil {
|
|
return e.msg
|
|
}
|
|
return e.msg + ": " + e.error.Error()
|
|
}
|
|
|
|
// LogValue implements slog.LogValuer. It returns a slog.GroupValue,
|
|
// having attributes "msg" and "type". If the error has a cause that
|
|
// from outside this package, the cause's type is included in a
|
|
// "cause" attribute.
|
|
func (e *errz) LogValue() slog.Value {
|
|
if e == nil {
|
|
return slog.Value{}
|
|
}
|
|
|
|
attrs := make([]slog.Attr, 2, 3)
|
|
attrs[0] = slog.String("msg", e.Error())
|
|
attrs[1] = slog.String("type", fmt.Sprintf("%T", e))
|
|
|
|
if cause := e.alienCause(); cause != nil {
|
|
attrs = append(attrs, slog.String("cause", fmt.Sprintf("%T", cause)))
|
|
}
|
|
|
|
return slog.GroupValue(attrs...)
|
|
}
|
|
|
|
// alienCause returns the first error in the chain that is
|
|
// not of type *errz, or returns nil if no such error.
|
|
func (e *errz) alienCause() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
|
|
inner := e.error
|
|
for inner != nil {
|
|
// Note: don't use errors.As here; we want the direct type assertion.
|
|
if v, ok := inner.(*errz); ok { //nolint:errorlint
|
|
inner = v.error
|
|
continue
|
|
}
|
|
return inner
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unwrap provides compatibility for Go 1.13 error chains.
|
|
func (e *errz) Unwrap() error { return e.error }
|
|
|
|
// Format implements fmt.Formatter.
|
|
func (e *errz) Format(s fmt.State, verb rune) {
|
|
switch verb {
|
|
case 'v':
|
|
if s.Flag('+') {
|
|
if e.error == nil {
|
|
_, _ = io.WriteString(s, e.msg)
|
|
e.stack.Format(s, verb)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(s, "%+v", e.error)
|
|
e.stack.Format(s, verb)
|
|
|
|
return
|
|
}
|
|
fallthrough
|
|
case 's':
|
|
if e.error == nil {
|
|
_, _ = io.WriteString(s, e.msg)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(s, e.Error())
|
|
case 'q':
|
|
if e.error == nil {
|
|
_, _ = fmt.Fprintf(s, "{%s}", e.msg)
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(s, "{%s}", e.Error())
|
|
}
|
|
}
|
|
|
|
// Wrap returns an error annotating err with a stack trace
|
|
// at the point Wrap is called, and the supplied message.
|
|
// If err is nil, Wrap returns nil. See also: Wrapf.
|
|
func Wrap(err error, message string) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
return &errz{stack: callers(0), error: err, msg: message}
|
|
}
|
|
|
|
// Wrapf returns an error annotating err with a stack trace
|
|
// at the point Wrapf is called. Wrapf will panic if format
|
|
// includes the %w verb: use errz.Errorf for that situation.
|
|
// If err is nil, Wrapf returns nil. See also: Wrap, Errorf.
|
|
func Wrapf(err error, format string, args ...any) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(format, "%w") {
|
|
panic("errz.Wrapf does not support %w verb: use errz.Errorf instead")
|
|
}
|
|
|
|
return &errz{error: err, msg: fmt.Sprintf(format, args...), stack: callers(0)}
|
|
}
|
|
|
|
// UnwrapChain returns the underlying *root* cause of the error. That is
|
|
// to say, UnwrapChain returns the final non-nil error in the error chain.
|
|
// UnwrapChain returns nil if err is nil.
|
|
func UnwrapChain(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var cause error
|
|
for {
|
|
if cause = errors.Unwrap(err); cause == nil {
|
|
break
|
|
}
|
|
err = cause
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Return returns t with err wrapped via errz.Err.
|
|
// This is useful for the common case of returning a value and
|
|
// an error from a function.
|
|
//
|
|
// written, err = errz.Return(io.Copy(w, r))
|
|
func Return[T any](t T, err error) (T, error) {
|
|
return t, Err(err)
|
|
}
|
|
|
|
// As is a convenience wrapper around errors.As.
|
|
//
|
|
// _, err := os.Open("non-existing")
|
|
// pathErr, ok := errz.As[*fs.PathError](err)
|
|
// require.True(t, ok)
|
|
// require.Equal(t, "non-existing", pathErr.Path)
|
|
//
|
|
// If err is nil, As returns false. See also: errz.Has.
|
|
func As[E error](err error) (E, bool) {
|
|
var target E
|
|
if err == nil {
|
|
return target, false
|
|
}
|
|
|
|
if errors.As(err, &target) {
|
|
return target, true
|
|
}
|
|
return target, false
|
|
}
|
|
|
|
// Has returns true if err, or an error in its error tree, matches error type E.
|
|
// An error is considered a match by the rules of errors.As:
|
|
//
|
|
// f, err := os.Open("non-existing")
|
|
// if errz.Has[*fs.PathError](err) {
|
|
// // Do something
|
|
// }
|
|
//
|
|
// If err is nil, Has returns false. See also: errz.As.
|
|
func Has[E error](err error) bool {
|
|
return errors.As(err, new(E))
|
|
}
|
|
|
|
// Chain returns a slice of all the errors in err's tree.
|
|
func Chain(err error) []error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
for err != nil {
|
|
errs = append(errs, err)
|
|
err = errors.Unwrap(err)
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// ExitCoder is an interface that an error type can implement to indicate
|
|
// that the program should exit with a specific status code.
|
|
// In particular, note that *exec.ExitError implements this interface.
|
|
type ExitCoder interface {
|
|
// ExitCode returns the exit code indicated by the error, or -1 if
|
|
// the error does not indicate a particular exit code.
|
|
ExitCode() int
|
|
}
|
|
|
|
// ExitCode returns the exit code of the first error in err's chain
|
|
// that implements ExitCoder, otherwise -1.
|
|
func ExitCode(err error) (code int) {
|
|
if err == nil {
|
|
return -1
|
|
}
|
|
|
|
chain := Chain(err)
|
|
for i := range chain {
|
|
if coder, ok := chain[i].(ExitCoder); ok { //nolint:errorlint
|
|
return coder.ExitCode()
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Drain reads all currently available non-nil errors from errCh. If errCh is
|
|
// nil, or there are no errors to read, Drain returns nil. If there's only a
|
|
// single error, Drain returns it. If there are multiple errors, Drain returns
|
|
// a multi-error created via [errz.Append]. Drain does not block; it does not
|
|
// wait for errCh to be closed. Thus, invoking Drain again on the same errCh
|
|
// may yield additional errors.
|
|
func Drain(errCh <-chan error) error {
|
|
if errCh == nil {
|
|
return nil
|
|
}
|
|
|
|
var result error
|
|
for {
|
|
select {
|
|
case err, ok := <-errCh:
|
|
if err != nil {
|
|
result = Append(result, err)
|
|
}
|
|
if !ok {
|
|
return result
|
|
}
|
|
default:
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
// SprintTreeTypes returns a string representation of err's type tree.
|
|
// A multi-error is represented as a slice of its children.
|
|
func SprintTreeTypes(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
errChain := Chain(err)
|
|
var sb strings.Builder
|
|
for i, e := range errChain {
|
|
sb.WriteString(fmt.Sprintf("%T", e))
|
|
if me, ok := e.(multipleErrorer); ok { //nolint:errorlint
|
|
children := me.Unwrap()
|
|
childText := make([]string, len(children))
|
|
for j := range children {
|
|
childText[j] = SprintTreeTypes(children[j])
|
|
}
|
|
joined := strings.Join(childText, ", ")
|
|
sb.WriteRune('[')
|
|
sb.WriteString(joined)
|
|
sb.WriteRune(']')
|
|
}
|
|
|
|
if i < len(errChain)-1 {
|
|
sb.WriteString(": ")
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// IsErrContext returns true if err's chain contains context.Canceled or
|
|
// context.DeadlineExceeded.
|
|
func IsErrContext(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return true
|
|
}
|
|
return errors.Is(err, context.DeadlineExceeded)
|
|
}
|