github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/errs/errs.go (about)

     1  package errs
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/ActiveState/cli/internal/condition"
    10  	"github.com/ActiveState/cli/internal/osutils/stacktrace"
    11  	"github.com/ActiveState/cli/internal/rtutils"
    12  	"gopkg.in/yaml.v3"
    13  )
    14  
    15  const TipMessage = "wrapped tips"
    16  
    17  type AsError interface {
    18  	As(interface{}) bool
    19  }
    20  
    21  // WrapperError enforces errors that include a stacktrace
    22  type Errorable interface {
    23  	Unwrap() error
    24  	Stack() *stacktrace.Stacktrace
    25  }
    26  
    27  type ErrorTips interface {
    28  	error
    29  	AddTips(...string)
    30  	ErrorTips() []string
    31  }
    32  
    33  // TransientError represents an error that is transient, meaning it does not itself represent a failure, but rather it
    34  // facilitates a mechanic meant to get to the actual error (eg. by wrapping or packing underlying errors).
    35  // Do NOT satisfy this interface for errors whose type you want to assert.
    36  type TransientError interface {
    37  	IsTransient()
    38  }
    39  
    40  // PackedErrors represents a collection of errors that aren't necessarily related to each other
    41  // note that rtutils replicates this functionality to avoid import cycles
    42  type PackedErrors struct {
    43  	errors []error
    44  }
    45  
    46  func (e *PackedErrors) IsTransient() {}
    47  
    48  func (e *PackedErrors) Error() string {
    49  	return "packed multiple errors"
    50  }
    51  
    52  func (e *PackedErrors) Unwrap() []error {
    53  	return e.errors
    54  }
    55  
    56  // WrapperError is what we use for errors created from this package, this does not mean every error returned from this
    57  // package is wrapping something, it simply has the plumbing to.
    58  type WrapperError struct {
    59  	message string
    60  	tips    []string
    61  	wrapped error
    62  	stack   *stacktrace.Stacktrace
    63  }
    64  
    65  func (e *WrapperError) Error() string {
    66  	return e.message
    67  }
    68  
    69  func (e *WrapperError) ErrorTips() []string {
    70  	return e.tips
    71  }
    72  
    73  func (e *WrapperError) AddTips(tips ...string) {
    74  	e.tips = append(e.tips, tips...)
    75  }
    76  
    77  // Unwrap returns the parent error, if one exists
    78  func (e *WrapperError) Unwrap() error {
    79  	return e.wrapped
    80  }
    81  
    82  // Stack returns the stacktrace for where this error was created
    83  func (e *WrapperError) Stack() *stacktrace.Stacktrace {
    84  	return e.stack
    85  }
    86  
    87  func newError(message string, wrapTarget error) *WrapperError {
    88  	return &WrapperError{
    89  		message,
    90  		[]string{},
    91  		wrapTarget,
    92  		stacktrace.GetWithSkip([]string{rtutils.CurrentFile()}),
    93  	}
    94  }
    95  
    96  // New creates a new error, similar to errors.New
    97  func New(message string, args ...interface{}) *WrapperError {
    98  	msg := fmt.Sprintf(message, args...)
    99  	return newError(msg, nil)
   100  }
   101  
   102  // Wrap creates a new error that wraps the given error
   103  func Wrap(wrapTarget error, message string, args ...interface{}) *WrapperError {
   104  	msg := fmt.Sprintf(message, args...)
   105  	return newError(msg, wrapTarget)
   106  }
   107  
   108  // Pack creates a new error that packs the given errors together, allowing for multiple errors to be returned
   109  func Pack(err error, errs ...error) error {
   110  	return &PackedErrors{append([]error{err}, errs...)}
   111  }
   112  
   113  // encodeErrorForJoin will recursively encode an error into a format that can be marshalled in a way that is easily
   114  // humanly readable.
   115  // In a nutshell the logic is:
   116  // - If the error is nil, return nil
   117  // - If the error is wrapped other errors, return it as a map with the key being the error and the value being the wrapped error(s)
   118  // - If the error is packing other errors, return them as a list of errors
   119  func encodeErrorForJoin(err error) interface{} {
   120  	if err == nil {
   121  		return nil
   122  	}
   123  
   124  	// If the error is a wrapper, unwrap it and encode the wrapped error
   125  	if u, ok := err.(unwrapNext); ok {
   126  		subErr := u.Unwrap()
   127  		if subErr == nil {
   128  			return err.Error()
   129  		}
   130  		return map[string]interface{}{err.Error(): encodeErrorForJoin(subErr)}
   131  	}
   132  
   133  	// If the error is a packer, encode the packed errors as a list
   134  	if u, ok := err.(unwrapPacked); ok {
   135  		var result []interface{}
   136  		// Don't encode errors that are transient as the real errors are kept underneath
   137  		if _, isTransient := err.(TransientError); !isTransient {
   138  			result = append(result, err.Error())
   139  		}
   140  		errs := u.Unwrap()
   141  		for _, nextErr := range errs {
   142  			result = append(result, encodeErrorForJoin(nextErr))
   143  		}
   144  		if len(result) == 1 {
   145  			return result[0]
   146  		}
   147  		return result
   148  	}
   149  
   150  	return err.Error()
   151  }
   152  
   153  func JoinMessage(err error) string {
   154  	v, err := yaml.Marshal(encodeErrorForJoin(err))
   155  	if err != nil {
   156  		// This shouldn't happen since we know exactly what we are marshalling
   157  		return fmt.Sprintf("failed to marshal error: %s", err)
   158  	}
   159  	return strings.TrimSpace(string(v))
   160  }
   161  
   162  func AddTips(err error, tips ...string) error {
   163  	var errTips ErrorTips
   164  	// MultiError uses a custom type to wrap multiple errors, so the type casting above won't work.
   165  	// Instead it satisfied `errors.As()`, but here we want to specifically check the current error and not any wrapped errors.
   166  	if asError, ok := err.(AsError); ok {
   167  		asError.As(&errTips)
   168  	}
   169  	if _, ok := err.(ErrorTips); ok {
   170  		errTips = err.(ErrorTips)
   171  	}
   172  	if errTips == nil {
   173  		// use original error message with identifier in case this bubbles all the way up
   174  		// this helps us identify it on rollbar without affecting the UX too much
   175  		errTips = newError(TipMessage, err)
   176  		err = errTips
   177  	}
   178  	errTips.AddTips(tips...)
   179  	return err
   180  }
   181  
   182  var errorType = reflect.TypeOf((*error)(nil)).Elem()
   183  
   184  // Matches is an analog for errors.As that just checks whether err matches the given type, so you can do:
   185  // errs.Matches(err, &ErrStruct{})
   186  // Without having to first assign it to a variable
   187  // This is useful if you ONLY care about the bool return value and not about setting the variable
   188  func Matches(err error, target interface{}) bool {
   189  	if target == nil {
   190  		panic("errors: target cannot be nil")
   191  	}
   192  
   193  	// Guard against miss-use of this function
   194  	if _, ok := target.(*WrapperError); ok {
   195  		if condition.BuiltOnDevMachine() || condition.InActiveStateCI() {
   196  			panic("target cannot be a WrapperError, you probably want errors.Is")
   197  		}
   198  	}
   199  
   200  	val := reflect.ValueOf(target)
   201  	targetType := val.Type()
   202  	if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) {
   203  		panic("errors: *target must be interface or implement error")
   204  	}
   205  	errs := Unpack(err)
   206  	for _, err := range errs {
   207  		if reflect.TypeOf(err).AssignableTo(targetType) {
   208  			return true
   209  		}
   210  		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(&target) {
   211  			return true
   212  		}
   213  	}
   214  	return false
   215  }
   216  
   217  func IsAny(err error, errs ...error) bool {
   218  	for _, e := range errs {
   219  		if errors.Is(err, e) {
   220  			return true
   221  		}
   222  	}
   223  	return false
   224  }
   225  
   226  type unwrapNext interface {
   227  	Unwrap() error
   228  }
   229  
   230  type unwrapPacked interface {
   231  	Unwrap() []error
   232  }
   233  
   234  // Unpack will recursively unpack an error into a list of errors, which is useful if you need to iterate over all errors.
   235  // This is similar to errors.Unwrap, but will also "unwrap" errors that are packed together, which errors.Unwrap does not.
   236  func Unpack(err error) []error {
   237  	result := []error{}
   238  
   239  	// add is a little helper function to add errors to the result, skipping any transient errors
   240  	add := func(errors ...error) {
   241  		for _, err := range errors {
   242  			if _, isTransient := err.(TransientError); isTransient {
   243  				continue
   244  			}
   245  			result = append(result, err)
   246  		}
   247  	}
   248  
   249  	// recursively unpack the error
   250  	for err != nil {
   251  		add(err)
   252  		if u, ok := err.(unwrapNext); ok {
   253  			// The error implements `Unwrap() error`, so simply unwrap it and continue the loop
   254  			err = u.Unwrap() // The next iteration will add the error to the result
   255  			continue
   256  		} else if u, ok := err.(unwrapPacked); ok {
   257  			// The error implements `Unwrap() []error`, so just add the resulting errors to the result and break the loop
   258  			errs := u.Unwrap()
   259  			for _, e := range errs {
   260  				add(Unpack(e)...)
   261  			}
   262  			break
   263  		} else {
   264  			break // nothing to unpack
   265  		}
   266  	}
   267  	return result
   268  }
   269  
   270  type ExternalError interface {
   271  	ExternalError() bool
   272  }
   273  
   274  func IsExternalError(err error) bool {
   275  	if err == nil {
   276  		return false
   277  	}
   278  
   279  	for _, err := range Unpack(err) {
   280  		errExternal, ok := err.(ExternalError)
   281  		if ok && errExternal.ExternalError() {
   282  			return true
   283  		}
   284  	}
   285  
   286  	return false
   287  }