github.com/haraldrudell/parl@v0.4.176/perrors/errorglue/chain-string.go (about)

     1  /*
     2  © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package errorglue
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"strings"
    12  
    13  	"github.com/haraldrudell/parl/pruntime"
    14  )
    15  
    16  const (
    17  	// string prepended to code location: “ at ”
    18  	atStringChain = "\x20at\x20"
    19  	// the string error value for error nil “OK”
    20  	errorIsNilString = "OK"
    21  )
    22  
    23  // ChainString() gets a string representation of a single error chain
    24  // TODO 220319 finish comment
    25  func ChainString(err error, format CSFormat) (s string) {
    26  
    27  	// no error case
    28  	if err == nil {
    29  		return errorIsNilString // no error return "OK"
    30  	}
    31  
    32  	switch format {
    33  	case DefaultFormat: // like printf %v, printf %s and error.Error()
    34  		s = err.Error() // the first error in the chain has our error message
    35  		return
    36  	case CodeLocation: // only one errror, with code location
    37  		s = shortFormat(err)
    38  		return
    39  	case ShortFormat: // one-liner with code location and associated errors
    40  		s = shortFormat(err)
    41  
    42  		//add appended errors at the end 2[…]
    43  		var list = ErrorList(err)
    44  		if len(list) > 1 {
    45  			list = list[1:] // skip err itself
    46  			var sList = make([]string, len(list))
    47  			for i, e := range list {
    48  				sList[i] = shortFormat(e)
    49  			}
    50  			s += fmt.Sprintf(" %d[%s]", len(list), strings.Join(sList, ", "))
    51  		}
    52  
    53  		return
    54  	case ShortSuffix: // for stackError, this provide the code-location without leading “ at ”
    55  		s = codeLocation(err)
    56  		return
    57  	case LongFormat:
    58  		// all errors with message and type
    59  		//	- stack traces
    60  		//	- related data
    61  		//	- associated errors
    62  	case LongSuffix:
    63  		// first error of each error-chain in long format
    64  		//	- an error chain is the initial error and any related errors
    65  		//	- stack traces and data for all errors
    66  	default:
    67  		var stack = pruntime.NewStack(0)
    68  		var packFuncS string
    69  		if len(stack.Frames()) > 0 {
    70  			packFuncS = stack.Frames()[0].Loc().PackFunc() + "\x20"
    71  		}
    72  		var e = fmt.Errorf("%sbad format: %s", packFuncS, format)
    73  		panic(NewErrorStack(e, stack))
    74  	}
    75  	// LongFormat, LongSuffix: recursive printing and associated errors
    76  
    77  	// errorMap is a map of errors already printed
    78  	//	- it is used to avoid cyclic printing
    79  	var errorMap = map[error]bool{}
    80  	// errorsToPrint: list of discovered associated errors to print
    81  	var errorsToPrint = []error{err}
    82  
    83  	// traverse all error instances
    84  	//	- the initial error and any unique related error
    85  	for i := 0; i < len(errorsToPrint); i++ {
    86  
    87  		// every error is an error chain
    88  		//	- traverse error chain
    89  		var isFirstInChain = true
    90  		for err = errorsToPrint[i]; err != nil; err = errors.Unwrap(err) {
    91  
    92  			// look for associated errors
    93  			if relatedHolder, ok := err.(RelatedError); ok {
    94  				if relatedErr := relatedHolder.AssociatedError(); relatedErr != nil {
    95  					// add any new errors to errorsToPrint
    96  					if !errorMap[relatedErr] {
    97  						errorMap[relatedErr] = true
    98  						errorsToPrint = append(errorsToPrint, relatedErr)
    99  					}
   100  				}
   101  			}
   102  
   103  			// ChainStringer errors produce their own representations
   104  			var errorAsString string
   105  			if richError, ok := err.(ChainStringer); ok {
   106  				if isFirstInChain {
   107  					errorAsString = richError.ChainString(LongFormat)
   108  				} else {
   109  					errorAsString = richError.ChainString(format)
   110  				}
   111  			} else {
   112  
   113  				// regular errors
   114  				//	- LongFormat prints all with type
   115  				//	- LongSuffix only prints if first in chain
   116  				if format == LongFormat || isFirstInChain {
   117  					errorAsString = fmt.Sprintf("%s [%T]", err.Error(), err)
   118  				}
   119  			}
   120  
   121  			if len(errorAsString) > 0 {
   122  				if len(s) > 0 {
   123  					s += "\n" + errorAsString
   124  				} else {
   125  					s = errorAsString
   126  				}
   127  			}
   128  			isFirstInChain = false
   129  		}
   130  	}
   131  
   132  	return
   133  }
   134  
   135  // shortFormat: “message at runtime/panic.go:914”
   136  //   - if err or its error-chain does not have location: “message” like [error.Error]
   137  //   - if err or its error-chain has panic, location is the code line
   138  //     that caused the first panic
   139  //   - if err or its error-chain has location but no panic,
   140  //     location is where the oldest error with stack was created
   141  //   - err is non-nil
   142  func shortFormat(err error) (s string) {
   143  
   144  	// append the top frame of the oldest, innermost stack trace code location
   145  	s = codeLocation(err)
   146  	if s != "" {
   147  		s = err.Error() + atStringChain + s
   148  	} else {
   149  		s = err.Error()
   150  	}
   151  
   152  	return
   153  }
   154  
   155  // codeLocation: “runtime/panic.go:914”
   156  //   - if err or its error-chain does not have location: empty string
   157  //   - if err or its error-chain has panic, location is the code line
   158  //     that caused the first panic
   159  //   - if err or its error-chain has location but no panic,
   160  //     location is where the oldest error with stack was created
   161  //   - err is non-nil, no “ at ” prefix
   162  func codeLocation(err error) (message string) {
   163  
   164  	// err or err’s error-chain may contain stacks
   165  	//	- any of the stacks may contain a panic
   166  	//	- an error with stack is able to locate any panic it or its chain has
   167  	//	- therefore scan for any error with stack and ask the first one for location
   168  	for e := err; e != nil; e = errors.Unwrap(e) {
   169  		if _, ok := e.(ErrorCallStacker); !ok {
   170  			continue // e does not have stack
   171  		}
   172  		var _ = (&errorStack{}).ChainString
   173  		message = e.(ChainStringer).ChainString(ShortSuffix)
   174  		return // found location return
   175  	}
   176  
   177  	return // no location return
   178  }
   179  
   180  // PrintfFormat gets the ErrorFormat to use when executing
   181  // the Printf value verb 'v'
   182  //   - %+v: [LongFormat]
   183  //   - %-v: [ShortFormat]
   184  //   - %v: [DefaultFormat]
   185  func PrintfFormat(s fmt.State) CSFormat {
   186  	if IsPlusFlag(s) {
   187  		return LongFormat
   188  	} else if IsMinusFlag(s) {
   189  		return ShortFormat
   190  	}
   191  	return DefaultFormat
   192  }