Skip to content

Commit f0a2a69

Browse files
authored
Merge pull request #115 from dhartunian/multierror-formatting
add formatting for multi-cause errors
2 parents 30a4e82 + 6adb34f commit f0a2a69

18 files changed

+10258
-64
lines changed

errbase/decode.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ func decodeLeaf(ctx context.Context, enc *errorspb.EncodedErrorLeaf) error {
5757
return genErr
5858
}
5959
// Decoding failed, we'll drop through to opaqueLeaf{} below.
60+
} else if decoder, ok := multiCauseDecoders[typeKey]; ok {
61+
causes := make([]error, len(enc.MultierrorCauses))
62+
for i, e := range enc.MultierrorCauses {
63+
causes[i] = DecodeError(ctx, *e)
64+
}
65+
genErr := decoder(ctx, causes, enc.Message, enc.Details.ReportablePayload, payload)
66+
if genErr != nil {
67+
return genErr
68+
}
6069
} else {
6170
// Shortcut for non-registered proto-encodable error types:
6271
// if it already implements `error`, it's good to go.
@@ -174,3 +183,24 @@ type WrapperDecoder = func(ctx context.Context, cause error, msgPrefix string, s
174183

175184
// registry for RegisterWrapperType.
176185
var decoders = map[TypeKey]WrapperDecoder{}
186+
187+
// MultiCauseDecoder is to be provided (via RegisterMultiCauseDecoder
188+
// above) by additional multi-cause wrapper types not yet known by the
189+
// library. A nil return indicates that decoding was not successful.
190+
type MultiCauseDecoder = func(ctx context.Context, causes []error, msgPrefix string, safeDetails []string, payload proto.Message) error
191+
192+
// registry for RegisterMultiCauseDecoder.
193+
var multiCauseDecoders = map[TypeKey]MultiCauseDecoder{}
194+
195+
// RegisterMultiCauseDecoder can be used to register new multi-cause
196+
// wrapper types to the library. Registered wrappers will be decoded
197+
// using their own Go type when an error is decoded. Multi-cause
198+
// wrappers that have not been registered will be decoded using the
199+
// opaqueWrapper type.
200+
func RegisterMultiCauseDecoder(theType TypeKey, decoder MultiCauseDecoder) {
201+
if decoder == nil {
202+
delete(multiCauseDecoders, theType)
203+
} else {
204+
multiCauseDecoders[theType] = decoder
205+
}
206+
}

errbase/encode.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,28 @@ type LeafEncoder = func(ctx context.Context, err error) (msg string, safeDetails
328328
// registry for RegisterLeafEncoder.
329329
var leafEncoders = map[TypeKey]LeafEncoder{}
330330

331+
// RegisterMultiCauseEncoder can be used to register new multi-cause
332+
// error types to the library. Registered types will be encoded using
333+
// their own Go type when an error is encoded. Multi-cause wrappers
334+
// that have not been registered will be encoded using the
335+
// opaqueWrapper type.
336+
func RegisterMultiCauseEncoder(theType TypeKey, encoder MultiCauseEncoder) {
337+
// This implementation is a simple wrapper around `LeafEncoder`
338+
// because we implemented multi-cause error wrapper encoding into a
339+
// `Leaf` instead of a `Wrapper` for smoother backwards
340+
// compatibility support. Exposing this detail to consumers of the
341+
// API is confusing and hence avoided. The causes of the error are
342+
// encoded separately regardless of this encoder's implementation.
343+
RegisterLeafEncoder(theType, encoder)
344+
}
345+
346+
// MultiCauseEncoder is to be provided (via RegisterMultiCauseEncoder
347+
// above) by additional multi-cause wrapper types not yet known to this
348+
// library. The encoder will automatically extract and encode the
349+
// causes of this error by calling `Unwrap()` and expecting a slice of
350+
// errors.
351+
type MultiCauseEncoder = func(ctx context.Context, err error) (msg string, safeDetails []string, payload proto.Message)
352+
331353
// RegisterWrapperEncoder can be used to register new wrapper types to
332354
// the library. Registered wrappers will be encoded using their own
333355
// Go type when an error is encoded. Wrappers that have not been

errbase/format_error.go

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,13 @@ func formatErrorInternal(err error, s fmt.State, verb rune, redactableOutput boo
102102
// to enable stack trace de-duplication. This requires a
103103
// post-order traversal. Since we have a linked list, the best we
104104
// can do is a recursion.
105-
p.formatRecursive(err, true /* isOutermost */, true /* withDetail */)
105+
p.formatRecursive(
106+
err,
107+
true, /* isOutermost */
108+
true, /* withDetail */
109+
false, /* withDepth */
110+
0, /* depth */
111+
)
106112

107113
// We now have all the data, we can render the result.
108114
p.formatEntries(err)
@@ -146,7 +152,13 @@ func formatErrorInternal(err error, s fmt.State, verb rune, redactableOutput boo
146152
// by calling FormatError(), in which case we'd get an infinite
147153
// recursion. So we have no choice but to peel the data
148154
// and then assemble the pieces ourselves.
149-
p.formatRecursive(err, true /* isOutermost */, false /* withDetail */)
155+
p.formatRecursive(
156+
err,
157+
true, /* isOutermost */
158+
false, /* withDetail */
159+
false, /* withDepth */
160+
0, /* depth */
161+
)
150162
p.formatSingleLineOutput()
151163
p.finishDisplay(verb)
152164

@@ -195,7 +207,19 @@ func (s *state) formatEntries(err error) {
195207
// Wraps: (N) <details>
196208
//
197209
for i, j := len(s.entries)-2, 2; i >= 0; i, j = i-1, j+1 {
198-
fmt.Fprintf(&s.finalBuf, "\nWraps: (%d)", j)
210+
s.finalBuf.WriteByte('\n')
211+
// Extra indentation starts at depth==2 because the direct
212+
// children of the root error area already printed on separate
213+
// newlines.
214+
for m := 0; m < s.entries[i].depth-1; m += 1 {
215+
if m == s.entries[i].depth-2 {
216+
s.finalBuf.WriteString("└─ ")
217+
} else {
218+
s.finalBuf.WriteByte(' ')
219+
s.finalBuf.WriteByte(' ')
220+
}
221+
}
222+
fmt.Fprintf(&s.finalBuf, "Wraps: (%d)", j)
199223
entry := s.entries[i]
200224
s.printEntry(entry)
201225
}
@@ -330,12 +354,34 @@ func (s *state) formatSingleLineOutput() {
330354
// s.finalBuf is untouched. The conversion of s.entries
331355
// to s.finalBuf is done by formatSingleLineOutput() and/or
332356
// formatEntries().
333-
func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
357+
//
358+
// `withDepth` and `depth` are used to tag subtrees of multi-cause
359+
// errors for added indentation during printing. Once a multi-cause
360+
// error is encountered, all subsequent calls with set `withDepth` to
361+
// true, and increment `depth` during recursion. This information is
362+
// persisted into the generated entries and used later to display the
363+
// error with increased indentation based in the depth.
364+
func (s *state) formatRecursive(err error, isOutermost, withDetail, withDepth bool, depth int) int {
334365
cause := UnwrapOnce(err)
366+
numChildren := 0
335367
if cause != nil {
336-
// Recurse first.
337-
s.formatRecursive(cause, false /*isOutermost*/, withDetail)
368+
// Recurse first, which populates entries list starting from innermost
369+
// entry. If we've previously seen a multi-cause wrapper, `withDepth`
370+
// will be true, and we'll record the depth below ensuring that extra
371+
// indentation is applied to this inner cause during printing.
372+
// Otherwise, we maintain "straight" vertical formatting by keeping the
373+
// parent callers `withDepth` value of `false` by default.
374+
numChildren += s.formatRecursive(cause, false, withDetail, withDepth, depth+1)
375+
}
376+
377+
causes := UnwrapMulti(err)
378+
for _, c := range causes {
379+
// Override `withDepth` to true for all child entries ensuring they have
380+
// indentation applied during formatting to distinguish them from
381+
// parents.
382+
numChildren += s.formatRecursive(c, false, withDetail, true, depth+1)
338383
}
384+
// inserted := len(s.entries) - 1 - startChildren
339385

340386
// Reinitialize the state for this stage of wrapping.
341387
s.wantDetail = withDetail
@@ -355,17 +401,19 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
355401
bufIsRedactable = true
356402
desiredShortening := v.SafeFormatError((*safePrinter)(s))
357403
if desiredShortening == nil {
358-
// The error wants to elide the short messages from inner
359-
// causes. Do it.
360-
s.elideFurtherCauseMsgs()
404+
// The error wants to elide the short messages from inner causes.
405+
// Read backwards through list of entries up to the number of new
406+
// entries created "under" this one amount and mark `elideShort`
407+
// true.
408+
s.elideShortChildren(numChildren)
361409
}
362410

363411
case Formatter:
364412
desiredShortening := v.FormatError((*printer)(s))
365413
if desiredShortening == nil {
366414
// The error wants to elide the short messages from inner
367415
// causes. Do it.
368-
s.elideFurtherCauseMsgs()
416+
s.elideShortChildren(numChildren)
369417
}
370418

371419
case fmt.Formatter:
@@ -389,7 +437,7 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
389437
if elideCauseMsg := s.formatSimple(err, cause); elideCauseMsg {
390438
// The error wants to elide the short messages from inner
391439
// causes. Do it.
392-
s.elideFurtherCauseMsgs()
440+
s.elideShortChildren(numChildren)
393441
}
394442
}
395443

@@ -412,7 +460,7 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
412460
if desiredShortening == nil {
413461
// The error wants to elide the short messages from inner
414462
// causes. Do it.
415-
s.elideFurtherCauseMsgs()
463+
s.elideShortChildren(numChildren)
416464
}
417465
break
418466
}
@@ -421,16 +469,21 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
421469
// If the error did not implement errors.Formatter nor
422470
// fmt.Formatter, but it is a wrapper, still attempt best effort:
423471
// print what we can at this level.
424-
if elideCauseMsg := s.formatSimple(err, cause); elideCauseMsg {
472+
elideChildren := s.formatSimple(err, cause)
473+
// always elideChildren when dealing with multi-cause errors.
474+
if len(causes) > 0 {
475+
elideChildren = true
476+
}
477+
if elideChildren {
425478
// The error wants to elide the short messages from inner
426479
// causes. Do it.
427-
s.elideFurtherCauseMsgs()
480+
s.elideShortChildren(numChildren)
428481
}
429482
}
430483
}
431484

432485
// Collect the result.
433-
entry := s.collectEntry(err, bufIsRedactable)
486+
entry := s.collectEntry(err, bufIsRedactable, withDepth, depth)
434487

435488
// If there's an embedded stack trace, also collect it.
436489
// This will get either a stack from pkg/errors, or ours.
@@ -444,21 +497,22 @@ func (s *state) formatRecursive(err error, isOutermost, withDetail bool) {
444497
// Remember the entry for later rendering.
445498
s.entries = append(s.entries, entry)
446499
s.buf = bytes.Buffer{}
500+
501+
return numChildren + 1
447502
}
448503

449-
// elideFurtherCauseMsgs sets the `elideShort` field
450-
// on all entries added so far to `true`. Because these
451-
// entries are added recursively from the innermost
452-
// cause outward, we can iterate through all entries
453-
// without bound because the caller is guaranteed not
454-
// to see entries that it is the causer of.
455-
func (s *state) elideFurtherCauseMsgs() {
456-
for i := range s.entries {
457-
s.entries[i].elideShort = true
504+
// elideShortChildren takes a number of entries to set `elideShort` to
505+
// false. The reason a number of entries is needed is that we may be
506+
// eliding a subtree of causes in the case of a multi-cause error. In
507+
// the multi-cause case, we need to know how many of the prior errors
508+
// in the list of entries is a child of this subtree.
509+
func (s *state) elideShortChildren(newEntries int) {
510+
for i := 0; i < newEntries; i++ {
511+
s.entries[len(s.entries)-1-i].elideShort = true
458512
}
459513
}
460514

461-
func (s *state) collectEntry(err error, bufIsRedactable bool) formatEntry {
515+
func (s *state) collectEntry(err error, bufIsRedactable bool, withDepth bool, depth int) formatEntry {
462516
entry := formatEntry{err: err}
463517
if s.wantDetail {
464518
// The buffer has been populated as a result of formatting with
@@ -495,6 +549,10 @@ func (s *state) collectEntry(err error, bufIsRedactable bool) formatEntry {
495549
}
496550
}
497551

552+
if withDepth {
553+
entry.depth = depth
554+
}
555+
498556
return entry
499557
}
500558

@@ -712,6 +770,11 @@ type formatEntry struct {
712770
// truncated to avoid duplication of entries. This is used to
713771
// display a truncation indicator during verbose rendering.
714772
elidedStackTrace bool
773+
774+
// depth, if positive, represents a nesting depth of this error as
775+
// a causer of others. This is used with verbose printing to
776+
// illustrate the nesting depth for multi-cause error wrappers.
777+
depth int
715778
}
716779

717780
// String is used for debugging only.
@@ -733,6 +796,12 @@ func (s *state) Write(b []byte) (n int, err error) {
733796

734797
for i, c := range b {
735798
if c == '\n' {
799+
//if s.needNewline > 0 {
800+
// for i := 0; i < s.needNewline-1; i++ {
801+
// s.buf.Write(detailSep[:len(sep)-1])
802+
// }
803+
// s.needNewline = 0
804+
//}
736805
// Flush all the bytes seen so far.
737806
s.buf.Write(b[k:i])
738807
// Don't print the newline itself; instead, prepare the state so
@@ -762,6 +831,11 @@ func (s *state) Write(b []byte) (n int, err error) {
762831
s.notEmpty = true
763832
}
764833
}
834+
//if s.needNewline > 0 {
835+
// for i := 0; i < s.needNewline-1; i++ {
836+
// s.buf.Write(detailSep[:len(sep)-1])
837+
// }
838+
//}
765839
s.buf.Write(b[k:])
766840
return len(b), nil
767841
}
@@ -788,6 +862,9 @@ func (p *state) switchOver() {
788862
p.buf = bytes.Buffer{}
789863
p.notEmpty = false
790864
p.hasDetail = true
865+
866+
// One of the newlines is accounted for in the switch over.
867+
// p.needNewline -= 1
791868
}
792869

793870
func (s *printer) Detail() bool {

0 commit comments

Comments
 (0)