errors/fmt: implementation of formatting interfaces
Change-Id: I9599b53a9c593aa560dd9fc3bd2d2cd0912afeae
Reviewed-on: https://go-review.googlesource.com/c/139500
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/errors/fmt/errors.go b/errors/fmt/errors.go
new file mode 100644
index 0000000..fbe8579
--- /dev/null
+++ b/errors/fmt/errors.go
@@ -0,0 +1,148 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fmt
+
+import (
+ "golang.org/x/exp/errors"
+)
+
+// fmtError formats err according to verb, writing to p.
+// If it cannot handle the error, it does no formatting
+// and returns false.
+func fmtError(p *pp, verb rune, err error) (handled bool) {
+ var (
+ sep = ": " // separator before next error
+ w = p // print buffer where error text is written
+ )
+ switch {
+ // Note that this switch must match the preference order
+ // for ordinary string printing (%#v before %+v, and so on).
+
+ case p.fmt.sharpV:
+ if stringer, ok := p.arg.(GoStringer); ok {
+ // Print the result of GoString unadorned.
+ p.fmt.fmtS(stringer.GoString())
+ return true
+ }
+ return false
+
+ case p.fmt.plusV:
+ sep = "\n--- "
+ w.fmt.fmtFlags = fmtFlags{plusV: p.fmt.plusV} // only keep detail flag
+
+ // The width or precision of a detailed view could be the number of
+ // errors to print from a list.
+
+ default:
+ // Use an intermediate buffer in the rare cases that precision,
+ // truncation, or one of the alternative verbs (q, x, and X) are
+ // specified.
+ switch verb {
+ case 's', 'v':
+ if (!w.fmt.widPresent || w.fmt.wid == 0) && !w.fmt.precPresent {
+ break
+ }
+ fallthrough
+ case 'q', 'x', 'X':
+ w = newPrinter()
+ defer w.free()
+ default:
+ w.badVerb(verb)
+ return true
+ }
+ }
+
+loop:
+ for {
+ w.fmt.inDetail = false
+ switch v := err.(type) {
+ case errors.Formatter:
+ err = v.Format((*errPP)(w))
+ // TODO: This case is for supporting old error implementations.
+ // It may eventually disappear.
+ case interface{ FormatError(errors.Printer) error }:
+ err = v.FormatError((*errPP)(w))
+ case Formatter:
+ // Discard verb, but keep the flags. Discarding the verb prevents
+ // nested quoting and other unwanted behavior. Preserving flags
+ // recursively signals a request for detail, if interpreted as %+v.
+ w.fmt.fmtFlags = p.fmt.fmtFlags
+ if w.fmt.plusV {
+ v.Format((*errPPState)(w), 'v') // indent new lines
+ } else {
+ v.Format(w, 'v') // do not indent new lines
+ }
+ break loop
+ default:
+ w.fmtString(v.Error(), 's')
+ break loop
+ }
+ if err == nil {
+ break
+ }
+ w.buf.WriteString(sep)
+ }
+
+ if w != p {
+ p.fmtString(string(w.buf), verb)
+ }
+ return true
+}
+
+// errPPState wraps a pp to implement State with indentation. It is used
+// for errors implementing fmt.Formatter.
+
+type errPPState pp
+
+func (p *errPPState) Width() (wid int, ok bool) { return (*pp)(p).Width() }
+func (p *errPPState) Precision() (prec int, ok bool) { return (*pp)(p).Precision() }
+func (p *errPPState) Flag(c int) bool { return (*pp)(p).Flag(c) }
+
+func (p *errPPState) Write(b []byte) (n int, err error) {
+ if !p.fmt.inDetail || p.fmt.plusV {
+ k := 0
+ if p.fmt.indent {
+ for i, c := range b {
+ if c == '\n' {
+ p.buf.Write(b[k:i])
+ p.buf.Write([]byte("\n "))
+ k = i + 1
+ }
+ }
+ }
+ p.buf.Write(b[k:])
+ }
+ return len(b), nil
+}
+
+// errPP wraps a pp to implement an errors.Printer.
+type errPP pp
+
+func (p *errPP) Print(args ...interface{}) {
+ if !p.fmt.inDetail || p.fmt.plusV {
+ if p.fmt.indent {
+ Fprint((*errPPState)(p), args...)
+ } else {
+ (*pp)(p).doPrint(args)
+ }
+ }
+}
+
+func (p *errPP) Printf(format string, args ...interface{}) {
+ if !p.fmt.inDetail || p.fmt.plusV {
+ if p.fmt.indent {
+ Fprintf((*errPPState)(p), format, args...)
+ } else {
+ (*pp)(p).doPrintf(format, args)
+ }
+ }
+}
+
+func (p *errPP) Detail() bool {
+ p.fmt.indent = p.fmt.plusV
+ p.fmt.inDetail = true
+ (*errPPState)(p).Write([]byte("\n"))
+ return p.fmt.plusV
+}
diff --git a/errors/fmt/errors_test.go b/errors/fmt/errors_test.go
new file mode 100644
index 0000000..7b47440
--- /dev/null
+++ b/errors/fmt/errors_test.go
@@ -0,0 +1,262 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package fmt_test
+
+import (
+ "io"
+ "os"
+ "testing"
+
+ "golang.org/x/exp/errors"
+ "golang.org/x/exp/errors/fmt"
+)
+
+func TestErrorFormatter(t *testing.T) {
+ var (
+ simple = &wrapped{"simple", nil}
+ elephant = &wrapped{
+ "can't adumbrate elephant",
+ detailed{},
+ }
+ transition = &wrapped2{"elephant still on strike", detailed{}}
+ nonascii = &wrapped{"café", nil}
+ newline = &wrapped{"msg with\nnewline",
+ &wrapped{"and another\none", nil}}
+ fallback = &wrapped{"fallback", os.ErrNotExist}
+ oldAndNew = &wrapped{"new style", formatError("old style")}
+ )
+ testCases := []struct {
+ err error
+ fmt string
+ want string
+ }{{
+ err: simple,
+ fmt: "%s",
+ want: "simple",
+ }, {
+ err: elephant,
+ fmt: "%s",
+ want: "can't adumbrate elephant: out of peanuts",
+ }, {
+ err: simple,
+ fmt: "%+v",
+ want: "simple\n somefile.go:123",
+ }, {
+ err: elephant,
+ fmt: "%+v",
+ want: `can't adumbrate elephant
+ somefile.go:123
+--- out of peanuts
+ the elephant is on strike
+ and the 12 monkeys
+ are laughing`,
+ }, {
+ err: transition,
+ fmt: "%+v",
+ want: `elephant still on strike
+ somefile.go:123
+--- out of peanuts
+ the elephant is on strike
+ and the 12 monkeys
+ are laughing`,
+ }, {
+ err: simple,
+ fmt: "%#v",
+ want: "&fmt_test.wrapped{msg:\"simple\", err:error(nil)}",
+ }, {
+ err: fmtTwice("Hello World!"),
+ fmt: "%#v",
+ want: "2 times Hello World!",
+ }, {
+ err: fallback,
+ fmt: "%s",
+ want: "fallback: file does not exist",
+ }, {
+ err: fallback,
+ fmt: "%+v",
+ want: "fallback\n somefile.go:123\n--- file does not exist",
+ }, {
+ err: oldAndNew,
+ fmt: "%v",
+ want: "new style: old style",
+ }, {
+ err: oldAndNew,
+ fmt: "%q",
+ want: `"new style: old style"`,
+ }, {
+ err: oldAndNew,
+ fmt: "%+v",
+ // Note the extra indentation.
+ want: "new style\n somefile.go:123\n--- old style\n otherfile.go:456",
+ }, {
+ err: simple,
+ fmt: "%-12s",
+ want: "simple ",
+ }, {
+ // Don't use formatting flags for detailed view.
+ err: simple,
+ fmt: "%+12v",
+ want: "simple\n somefile.go:123",
+ }, {
+ err: elephant,
+ fmt: "%+50s",
+ want: " can't adumbrate elephant: out of peanuts",
+ }, {
+ err: nonascii,
+ fmt: "%q",
+ want: `"café"`,
+ }, {
+ err: nonascii,
+ fmt: "%+q",
+ want: `"caf\u00e9"`,
+ }, {
+ err: simple,
+ fmt: "% x",
+ want: "73 69 6d 70 6c 65",
+ }, {
+ err: newline,
+ fmt: "%s",
+ want: "msg with\nnewline: and another\none",
+ }, {
+ err: nil,
+ fmt: "%+v",
+ want: "<nil>",
+ }, {
+ err: (*wrapped)(nil),
+ fmt: "%+v",
+ want: "<nil>",
+ }, {
+ err: simple,
+ fmt: "%T",
+ want: "*fmt_test.wrapped",
+ }, {
+ err: simple,
+ fmt: "%🤪",
+ want: "%!🤪(*fmt_test.wrapped=&{simple <nil>})",
+ }, {
+ err: formatError("use fmt.Formatter"),
+ fmt: "%#v",
+ want: "use fmt.Formatter",
+ }, {
+ err: wrapped{"using errors.Formatter",
+ formatError("use fmt.Formatter")},
+ fmt: "%#v",
+ want: "fmt_test.wrapped{msg:\"using errors.Formatter\", err:\"use fmt.Formatter\"}",
+ }, {
+ err: fmtTwice("%s %s", "ok", panicValue{}),
+ fmt: "%s",
+ want: "ok %!s(PANIC=panic)/ok %!s(PANIC=panic)",
+ }, {
+ err: fmtTwice("%o %s", panicValue{}, "ok"),
+ fmt: "%s",
+ want: "{} ok/{} ok",
+ }}
+ for i, tc := range testCases {
+ t.Run(fmt.Sprintf("%d/%s", i, tc.fmt), func(t *testing.T) {
+ got := fmt.Sprintf(tc.fmt, tc.err)
+ if got != tc.want {
+ t.Errorf("\n got: %q\nwant: %q", got, tc.want)
+ }
+ })
+ }
+}
+
+var _ errors.Formatter = wrapped{}
+
+type wrapped struct {
+ msg string
+ err error
+}
+
+func (e wrapped) Error() string { return "should call Format" }
+
+func (e wrapped) Format(p errors.Printer) (next error) {
+ p.Print(e.msg)
+ p.Detail()
+ p.Print("somefile.go:123")
+ return e.err
+}
+
+type wrapped2 struct {
+ msg string
+ err error
+}
+
+func (e wrapped2) Error() string { return "should call Format" }
+
+func (e wrapped2) FormatError(p errors.Printer) (next error) {
+ p.Print(e.msg)
+ p.Detail()
+ p.Print("somefile.go:123")
+ return e.err
+}
+
+var _ errors.Formatter = detailed{}
+
+type detailed struct{}
+
+func (e detailed) Error() string { return fmt.Sprint(e) }
+
+func (detailed) Format(p errors.Printer) (next error) {
+ p.Printf("out of %s", "peanuts")
+ p.Detail()
+ p.Print("the elephant is on strike\n")
+ p.Printf("and the %d monkeys\nare laughing", 12)
+ return nil
+}
+
+// formatError is an error implementing Format instead of errors.Formatter.
+// The implementation mimics the implementation of github.com/pkg/errors,
+// including that
+type formatError string
+
+func (e formatError) Error() string { return string(e) }
+
+func (e formatError) Format(s fmt.State, verb rune) {
+ // Body based on pkg/errors/errors.go
+ switch verb {
+ case 'v':
+ if s.Flag('+') {
+ io.WriteString(s, string(e))
+ fmt.Fprintf(s, "\n%s", "otherfile.go:456")
+ return
+ }
+ fallthrough
+ case 's':
+ io.WriteString(s, string(e))
+ case 'q':
+ fmt.Fprintf(s, "%q", string(e))
+ }
+}
+
+func (e formatError) GoString() string {
+ panic("should never be called")
+}
+
+type fmtTwiceErr struct {
+ format string
+ args []interface{}
+}
+
+func fmtTwice(format string, a ...interface{}) error {
+ return fmtTwiceErr{format, a}
+}
+
+func (e fmtTwiceErr) Error() string { return fmt.Sprint(e) }
+
+func (e fmtTwiceErr) Format(p errors.Printer) (next error) {
+ p.Printf(e.format, e.args...)
+ p.Print("/")
+ p.Printf(e.format, e.args...)
+ return nil
+}
+
+func (e fmtTwiceErr) GoString() string {
+ return "2 times " + fmt.Sprintf(e.format, e.args...)
+}
+
+type panicValue struct{}
+
+func (panicValue) String() string { panic("panic") }
diff --git a/errors/fmt/format.go b/errors/fmt/format.go
index 91103f2..83872fe 100644
--- a/errors/fmt/format.go
+++ b/errors/fmt/format.go
@@ -34,6 +34,10 @@
// different, flagless formats set at the top level.
plusV bool
sharpV bool
+
+ // error-related flags. plusV indicates detail mode.
+ inDetail bool
+ indent bool
}
// A fmt is the raw formatter used by Printf etc.
diff --git a/errors/fmt/print.go b/errors/fmt/print.go
index f67f805..8af4fef 100644
--- a/errors/fmt/print.go
+++ b/errors/fmt/print.go
@@ -570,6 +570,11 @@
formatter.Format(p, verb)
return
}
+ if err, ok := p.arg.(error); ok {
+ handled = true
+ defer p.catchPanic(p.arg, verb)
+ return fmtError(p, verb, err)
+ }
// If we're doing Go syntax and the argument knows how to supply it, take care of it now.
if p.fmt.sharpV {
@@ -586,18 +591,7 @@
// Println etc. set verb to %v, which is "stringable".
switch verb {
case 'v', 's', 'x', 'X', 'q':
- // Is it an error or Stringer?
- // The duplication in the bodies is necessary:
- // setting handled and deferring catchPanic
- // must happen before calling the method.
- switch v := p.arg.(type) {
- case error:
- handled = true
- defer p.catchPanic(p.arg, verb)
- p.fmtString(v.Error(), verb)
- return
-
- case Stringer:
+ if v, ok := p.arg.(Stringer); ok {
handled = true
defer p.catchPanic(p.arg, verb)
p.fmtString(v.String(), verb)