1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
|
package diag
import (
"fmt"
"strings"
"src.elv.sh/pkg/strutil"
)
// Error represents an error with context that can be showed.
type Error[T ErrorTag] struct {
Message string
Context Context
// Indicates whether the error may be caused by partial input. More
// formally, this field should be true iff there exists a string x such that
// appending it to the input eliminates the error.
Partial bool
}
// ErrorTag is used to parameterize [Error] into different concrete types. The
// ErrorTag method is called with a zero receiver, and its return value is used
// in [Error.Error] and [Error.Show].
type ErrorTag interface {
ErrorTag() string
}
// RangeError combines error with [Ranger].
type RangeError interface {
error
Ranger
}
// Error returns a plain text representation of the error.
func (e *Error[T]) Error() string {
return errorTag[T]() + ": " + e.errorNoType()
}
func (e *Error[T]) errorNoType() string {
return e.Context.describeRange() + ": " + e.Message
}
// Range returns the range of the error.
func (e *Error[T]) Range() Ranging {
return e.Context.Range()
}
var (
messageStart = "\033[31;1m"
messageEnd = "\033[m"
)
// Show shows the error.
func (e *Error[T]) Show(indent string) string {
return errorTagTitle[T]() + ": " + e.showNoType(indent)
}
func (e *Error[T]) showNoType(indent string) string {
indent += " "
return messageStart + e.Message + messageEnd +
"\n" + indent + e.Context.Show(indent)
}
// PackErrors packs multiple instances of [Error] with the same tag into one
// error:
//
// - If called with no errors, it returns nil.
//
// - If called with one error, it returns that error itself.
//
// - If called with more than one [Error], it returns an error that combines
// all of them. The returned error also implements [Shower], and its Error
// and Show methods only print the tag once.
func PackErrors[T ErrorTag](errs []*Error[T]) error {
switch len(errs) {
case 0:
return nil
case 1:
return errs[0]
default:
return append(multiError[T](nil), errs...)
}
}
// UnpackErrors returns the constituent [Error] instances in an error if it is
// built from [PackErrors]. Otherwise it returns nil.
func UnpackErrors[T ErrorTag](err error) []*Error[T] {
switch err := err.(type) {
case *Error[T]:
return []*Error[T]{err}
case multiError[T]:
return append([]*Error[T](nil), err...)
default:
return nil
}
}
type multiError[T ErrorTag] []*Error[T]
func (err multiError[T]) Error() string {
var sb strings.Builder
fmt.Fprintf(&sb, "multiple %s: ", errorTagPlural[T]())
for i, e := range err {
if i > 0 {
sb.WriteString("; ")
}
sb.WriteString(e.errorNoType())
}
return sb.String()
}
func (err multiError[T]) Show(indent string) string {
var sb strings.Builder
fmt.Fprintf(&sb, "Multiple %s:", errorTagPlural[T]())
indent += " "
for _, e := range err {
sb.WriteString("\n" + indent)
sb.WriteString(e.showNoType(indent))
}
return sb.String()
}
func errorTag[T ErrorTag]() string {
var t T
return t.ErrorTag()
}
// We don't have any error tags with an irregular plural yet. When we do, we can
// let ErrorTag optionally implement interface{ ErrorTagPlural() } and use that
// when available.
func errorTagPlural[T ErrorTag]() string { return errorTag[T]() + "s" }
func errorTagTitle[T ErrorTag]() string { return strutil.Title(errorTag[T]()) }
|