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 }