File: error.go

package info (click to toggle)
elvish 0.21.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,372 kB
  • sloc: javascript: 236; sh: 130; python: 104; makefile: 88; xml: 9
file content (132 lines) | stat: -rw-r--r-- 3,369 bytes parent folder | download
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]()) }