xerrors: copied files from x/exp/errors changed paths and removed experimental notice Also made panic test pass: the output differed for different Go versions. Change-Id: Iadd367fb253459d288c3ec7e3aaafce9af939983 Reviewed-on: https://go-review.googlesource.com/c/159777 Run-TryBot: Marcel van Lohuizen <mpvl@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/adaptor.go b/adaptor.go new file mode 100644 index 0000000..83a7945 --- /dev/null +++ b/adaptor.go
@@ -0,0 +1,188 @@ +// 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 xerrors + +import ( + "bytes" + "fmt" + "io" + "reflect" + "strconv" +) + +// FormatError calls the FormatError method of f with an errors.Printer +// configured according to s and verb, and writes the result to s. +func FormatError(f Formatter, s fmt.State, verb rune) { + // Assuming this function is only called from the Format method, and given + // that FormatError takes precedence over Format, it cannot be called from + // any package that supports errors.Formatter. It is therefore safe to + // disregard that State may be a specific printer implementation and use one + // of our choice instead. + + // limitations: does not support printing error as Go struct. + + var ( + sep = " " // separator before next error + p = &state{State: s} + direct = true + ) + + var err error = f + + switch verb { + // Note that this switch must match the preference order + // for ordinary string printing (%#v before %+v, and so on). + + case 'v': + if s.Flag('#') { + if stringer, ok := err.(fmt.GoStringer); ok { + io.WriteString(&p.buf, stringer.GoString()) + goto exit + } + // proceed as if it were %v + } else if s.Flag('+') { + p.printDetail = true + sep = "\n - " + } + case 's': + case 'q', 'x', 'X': + // Use an intermediate buffer in the rare cases that precision, + // truncation, or one of the alternative verbs (q, x, and X) are + // specified. + direct = false + + default: + p.buf.WriteString("%!") + p.buf.WriteRune(verb) + p.buf.WriteByte('(') + switch { + case err != nil: + p.buf.WriteString(reflect.TypeOf(f).String()) + default: + p.buf.WriteString("<nil>") + } + p.buf.WriteByte(')') + io.Copy(s, &p.buf) + return + } + +loop: + for { + p.inDetail = false + + switch v := err.(type) { + case Formatter: + err = v.FormatError((*printer)(p)) + case fmt.Formatter: + v.Format(p, 'v') + break loop + default: + io.WriteString(&p.buf, v.Error()) + break loop + } + if err == nil { + break + } + if !p.inDetail || !p.printDetail { + p.buf.WriteByte(':') + } + // Strip last newline of detail. + if bytes.HasSuffix(p.buf.Bytes(), detailSep) { + p.buf.Truncate(p.buf.Len() - len(detailSep)) + } + p.buf.WriteString(sep) + p.inDetail = false + } + +exit: + width, okW := s.Width() + prec, okP := s.Precision() + + if !direct || (okW && width > 0) || okP { + // Construct format string from State s. + format := []byte{'%'} + if s.Flag('-') { + format = append(format, '-') + } + if s.Flag('+') { + format = append(format, '+') + } + if s.Flag(' ') { + format = append(format, ' ') + } + if okW { + format = strconv.AppendInt(format, int64(width), 10) + } + if okP { + format = append(format, '.') + format = strconv.AppendInt(format, int64(prec), 10) + } + format = append(format, string(verb)...) + fmt.Fprintf(s, string(format), p.buf.String()) + } else { + io.Copy(s, &p.buf) + } +} + +var detailSep = []byte("\n ") + +// state tracks error printing state. It implements fmt.State. +type state struct { + fmt.State + buf bytes.Buffer + + printDetail bool + inDetail bool + needNewline bool +} + +func (s *state) Write(b []byte) (n int, err error) { + if s.printDetail { + if len(b) == 0 { + return 0, nil + } + if s.inDetail && s.needNewline { + s.needNewline = false + s.buf.WriteByte(':') + s.buf.Write(detailSep) + if b[0] == '\n' { + b = b[1:] + } + } + k := 0 + for i, c := range b { + if c == '\n' { + s.buf.Write(b[k:i]) + s.buf.Write(detailSep) + k = i + 1 + } + } + s.buf.Write(b[k:]) + s.needNewline = !s.inDetail + } else if !s.inDetail { + s.buf.Write(b) + } + return len(b), nil +} + +// printer wraps a state to implement an xerrors.Printer. +type printer state + +func (s *printer) Print(args ...interface{}) { + if !s.inDetail || s.printDetail { + fmt.Fprint((*state)(s), args...) + } +} + +func (s *printer) Printf(format string, args ...interface{}) { + if !s.inDetail || s.printDetail { + fmt.Fprintf((*state)(s), format, args...) + } +} + +func (s *printer) Detail() bool { + s.inDetail = true + return s.printDetail +}
diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..1ad48f5 --- /dev/null +++ b/doc.go
@@ -0,0 +1,25 @@ +// Copyright 2019 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 xerrors implements functions to manipulate errors. +// +// This package supports transitioning to the Go 2 proposal for error values: +// https://golang.org/design/29934-error-values +// +// Most of the functions and types in this package will be incorporated into the +// standard library's errors package in Go 1.13; the behavior of this package's +// Errorf function will be incorporated into the standard library's fmt.Errorf. +// Use this package to get equivalent behavior in all supported Go versions. For +// example, create errors using +// +// xerrors.New("write failed") +// +// or +// +// xerrors.Errorf("while reading: %v", err) +// +// If you want your error type to participate in the new formatting +// implementation for %v and %+v, provide it with a Format method that calls +// xerrors.FormatError, as shown in the example for FormatError. +package xerrors // import "golang.org/x/xerrors"
diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..0a812bd --- /dev/null +++ b/errors.go
@@ -0,0 +1,29 @@ +// Copyright 2011 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 xerrors + +// errorString is a trivial implementation of error. +type errorString struct { + s string + frame Frame +} + +// New returns an error that formats as the given text. +// +// The returned error contains a Frame set to the caller's location and +// implements Formatter to show this information when printed with details. +func New(text string) error { + return &errorString{text, Caller(1)} +} + +func (e *errorString) Error() string { + return e.s +} + +func (e *errorString) FormatError(p Printer) (next error) { + p.Print(e.s) + e.frame.Format(p) + return nil +}
diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..3417ee6 --- /dev/null +++ b/errors_test.go
@@ -0,0 +1,54 @@ +// Copyright 2011 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 xerrors_test + +import ( + "fmt" + "testing" + + "golang.org/x/xerrors" +) + +func TestNewEqual(t *testing.T) { + // Different allocations should not be equal. + if xerrors.New("abc") == xerrors.New("abc") { + t.Errorf(`New("abc") == New("abc")`) + } + if xerrors.New("abc") == xerrors.New("xyz") { + t.Errorf(`New("abc") == New("xyz")`) + } + + // Same allocation should be equal to itself (not crash). + err := xerrors.New("jkl") + if err != err { + t.Errorf(`err != err`) + } +} + +func TestErrorMethod(t *testing.T) { + err := xerrors.New("abc") + if err.Error() != "abc" { + t.Errorf(`New("abc").Error() = %q, want %q`, err.Error(), "abc") + } +} + +func ExampleNew() { + err := xerrors.New("emit macho dwarf: elf header corrupted") + if err != nil { + fmt.Print(err) + } + // Output: emit macho dwarf: elf header corrupted +} + +// The fmt package's Errorf function lets us use the package's formatting +// features to create descriptive error messages. +func ExampleNew_errorf() { + const name, id = "bimmler", 17 + err := fmt.Errorf("user %q (id %d) not found", name, id) + if err != nil { + fmt.Print(err) + } + // Output: user "bimmler" (id 17) not found +}
diff --git a/example_As_test.go b/example_As_test.go new file mode 100644 index 0000000..647afdd --- /dev/null +++ b/example_As_test.go
@@ -0,0 +1,25 @@ +// Copyright 2019 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 xerrors_test + +import ( + "fmt" + "os" + + "golang.org/x/xerrors" +) + +func ExampleAs() { + _, err := os.Open("non-existing") + if err != nil { + var pathError *os.PathError + if xerrors.As(err, &pathError) { + fmt.Println("Failed at path:", pathError.Path) + } + } + + // Output: + // Failed at path: non-existing +}
diff --git a/example_FormatError_test.go b/example_FormatError_test.go new file mode 100644 index 0000000..3e712c9 --- /dev/null +++ b/example_FormatError_test.go
@@ -0,0 +1,39 @@ +// Copyright 2019 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 xerrors_test + +import ( + "fmt" + + "golang.org/x/xerrors" +) + +type MyError2 struct { + Message string + frame xerrors.Frame +} + +func (m *MyError2) Error() string { + return m.Message +} + +func (m *MyError2) Format(f fmt.State, c rune) { // implements fmt.Formatter + xerrors.FormatError(m, f, c) +} + +func (m *MyError2) FormatError(p xerrors.Printer) error { // implements xerrors.Formatter + p.Print(m.Message) + if p.Detail() { + m.frame.Format(p) + } + return nil +} + +func ExampleFormatError() { + err := &MyError2{Message: "oops", frame: xerrors.Caller(1)} + fmt.Printf("%v\n", err) + fmt.Println() + fmt.Printf("%+v\n", err) +}
diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..107f80c --- /dev/null +++ b/example_test.go
@@ -0,0 +1,34 @@ +// Copyright 2012 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 xerrors_test + +import ( + "fmt" + "time" +) + +// MyError is an error implementation that includes a time and message. +type MyError struct { + When time.Time + What string +} + +func (e MyError) Error() string { + return fmt.Sprintf("%v: %v", e.When, e.What) +} + +func oops() error { + return MyError{ + time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC), + "the file system has gone away", + } +} + +func Example() { + if err := oops(); err != nil { + fmt.Println(err) + } + // Output: 1989-03-15 22:30:00 +0000 UTC: the file system has gone away +}
diff --git a/fmt.go b/fmt.go new file mode 100644 index 0000000..74c1c93 --- /dev/null +++ b/fmt.go
@@ -0,0 +1,109 @@ +// 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 xerrors + +import ( + "fmt" + "strings" + + "golang.org/x/xerrors/internal" +) + +// Errorf formats according to a format specifier and returns the string as a +// value that satisfies error. +// +// The returned error includes the file and line number of the caller when +// formatted with additional detail enabled. If the last argument is an error +// the returned error's Format method will return it if the format string ends +// with ": %s", ": %v", or ": %w". If the last argument is an error and the +// format string ends with ": %w", the returned error implements Wrapper +// with an Unwrap method returning it. +func Errorf(format string, a ...interface{}) error { + err, wrap := lastError(format, a) + format = formatPlusW(format) + if err == nil { + return &noWrapError{fmt.Sprintf(format, a...), nil, Caller(1)} + } + + // TODO: this is not entirely correct. The error value could be + // printed elsewhere in format if it mixes numbered with unnumbered + // substitutions. With relatively small changes to doPrintf we can + // have it optionally ignore extra arguments and pass the argument + // list in its entirety. + msg := fmt.Sprintf(format[:len(format)-len(": %s")], a[:len(a)-1]...) + frame := Frame{} + if internal.EnableTrace { + frame = Caller(1) + } + if wrap { + return &wrapError{msg, err, frame} + } + return &noWrapError{msg, err, frame} +} + +// formatPlusW is used to avoid the vet check that will barf at %w. +func formatPlusW(s string) string { + return s +} + +func lastError(format string, a []interface{}) (err error, wrap bool) { + wrap = strings.HasSuffix(format, ": %w") + if !wrap && + !strings.HasSuffix(format, ": %s") && + !strings.HasSuffix(format, ": %v") { + return nil, false + } + + if len(a) == 0 { + return nil, false + } + + err, ok := a[len(a)-1].(error) + if !ok { + return nil, false + } + + return err, wrap +} + +type noWrapError struct { + msg string + err error + frame Frame +} + +func (e *noWrapError) Error() string { + return fmt.Sprint(e) +} + +func (e *noWrapError) Format(s fmt.State, v rune) { FormatError(e, s, v) } + +func (e *noWrapError) FormatError(p Printer) (next error) { + p.Print(e.msg) + e.frame.Format(p) + return e.err +} + +type wrapError struct { + msg string + err error + frame Frame +} + +func (e *wrapError) Error() string { + return fmt.Sprint(e) +} + +func (e *wrapError) Format(s fmt.State, v rune) { FormatError(e, s, v) } + +func (e *wrapError) FormatError(p Printer) (next error) { + p.Print(e.msg) + e.frame.Format(p) + return e.err +} + +func (e *wrapError) Unwrap() error { + return e.err +}
diff --git a/fmt_test.go b/fmt_test.go new file mode 100644 index 0000000..e524285 --- /dev/null +++ b/fmt_test.go
@@ -0,0 +1,542 @@ +// 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 xerrors_test + +import ( + "fmt" + "io" + "os" + "path" + "reflect" + "regexp" + "strconv" + "strings" + "testing" + + "golang.org/x/xerrors" +) + +func TestErrorf(t *testing.T) { + chained := &wrapped{"chained", nil} + chain := func(s ...string) (a []string) { + for _, s := range s { + a = append(a, cleanPath(s)) + } + return a + } + testCases := []struct { + got error + want []string + }{{ + xerrors.Errorf("no args"), + chain("no args/path.TestErrorf/path.go:xxx"), + }, { + xerrors.Errorf("no args: %s"), + chain("no args: %!s(MISSING)/path.TestErrorf/path.go:xxx"), + }, { + xerrors.Errorf("nounwrap: %s", "simple"), + chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), + }, { + xerrors.Errorf("nounwrap: %v", "simple"), + chain(`nounwrap: simple/path.TestErrorf/path.go:xxx`), + }, { + xerrors.Errorf("%s failed: %v", "foo", chained), + chain("foo failed/path.TestErrorf/path.go:xxx", + "chained/somefile.go:xxx"), + }, { + xerrors.Errorf("no wrap: %s", chained), + chain("no wrap/path.TestErrorf/path.go:xxx", + "chained/somefile.go:xxx"), + }, { + xerrors.Errorf("%s failed: %w", "foo", chained), + chain("wraps:foo failed/path.TestErrorf/path.go:xxx", + "chained/somefile.go:xxx"), + }, { + xerrors.Errorf("nowrapv: %v", chained), + chain("nowrapv/path.TestErrorf/path.go:xxx", + "chained/somefile.go:xxx"), + }, { + xerrors.Errorf("wrapw: %w", chained), + chain("wraps:wrapw/path.TestErrorf/path.go:xxx", + "chained/somefile.go:xxx"), + }, { + xerrors.Errorf("not wrapped: %+v", chained), + chain("not wrapped: chained: somefile.go:123/path.TestErrorf/path.go:xxx"), + }} + for i, tc := range testCases { + t.Run(strconv.Itoa(i)+"/"+path.Join(tc.want...), func(t *testing.T) { + got := errToParts(tc.got) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("Format:\n got: %#v\nwant: %#v", got, tc.want) + } + + gotStr := tc.got.Error() + wantStr := fmt.Sprint(tc.got) + if gotStr != wantStr { + t.Errorf("Error:\n got: %#v\nwant: %#v", got, tc.want) + } + }) + } +} + +func TestErrorFormatter(t *testing.T) { + var ( + simple = &wrapped{"simple", nil} + elephant = &wrapped{ + "can't adumbrate elephant", + 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")} + framed = &withFrameAndMore{ + frame: xerrors.Caller(0), + } + opaque = &wrapped{"outer", + xerrors.Opaque(&wrapped{"mid", + &wrapped{"inner", nil}})} + ) + testCases := []struct { + err error + fmt string + want string + regexp bool + }{{ + err: simple, + fmt: "%s", + want: "simple", + }, { + err: elephant, + fmt: "%s", + want: "can't adumbrate elephant: out of peanuts", + }, { + err: &wrapped{"a", &wrapped{"b", &wrapped{"c", nil}}}, + fmt: "%s", + want: "a: b: c", + }, { + err: simple, + fmt: "%+v", + want: "simple:" + + "\n somefile.go:123", + }, { + err: elephant, + fmt: "%+v", + want: "can't adumbrate elephant:" + + "\n somefile.go:123" + + "\n - out of peanuts:" + + "\n the elephant is on strike" + + "\n and the 12 monkeys" + + "\n are laughing", + }, { + err: framed, + fmt: "%+v", + want: "something:" + + "\n golang.org/x/xerrors_test.TestErrorFormatter" + + "\n .+/golang.org/x/xerrors/fmt_test.go:97" + + "\n something more", + regexp: true, + }, { + 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", + // Note: no colon after the last error, as there are no details. + want: "fallback:" + + "\n somefile.go:123" + + "\n - file does not exist", + }, { + err: opaque, + fmt: "%s", + want: "outer: mid: inner", + }, { + err: opaque, + fmt: "%+v", + want: "outer:" + + "\n somefile.go:123" + + "\n - mid:" + + "\n somefile.go:123" + + "\n - inner:" + + "\n somefile.go:123", + }, { + 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. + // Colon for old style error is rendered by the fmt.Formatter + // implementation of the old-style error. + 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: newline, + fmt: "%+v", + want: "msg with" + + "\n newline:" + + "\n somefile.go:123" + + "\n - and another" + + "\n one:" + + "\n somefile.go:123", + }, { + err: &wrapped{"", &wrapped{"inner message", nil}}, + fmt: "%+v", + want: "somefile.go:123" + + "\n - inner message:" + + "\n somefile.go:123", + }, { + err: spurious(""), + fmt: "%s", + want: "spurious", + }, { + err: spurious(""), + fmt: "%+v", + want: "spurious", + }, { + err: spurious("extra"), + fmt: "%s", + want: "spurious", + }, { + err: spurious("extra"), + fmt: "%+v", + want: "spurious:\n" + + " extra", + }, { + err: nil, + fmt: "%+v", + want: "<nil>", + }, { + err: (*wrapped)(nil), + fmt: "%+v", + want: "<nil>", + }, { + err: simple, + fmt: "%T", + want: "*xerrors_test.wrapped", + }, { + err: simple, + fmt: "%🤪", + want: "%!🤪(*xerrors_test.wrapped)", + // For 1.13: + // want: "%!🤪(*xerrors_test.wrapped=&{simple <nil>})", + }, { + err: formatError("use fmt.Formatter"), + fmt: "%#v", + want: "use fmt.Formatter", + }, { + err: fmtTwice("%s %s", "ok", panicValue{}), + fmt: "%s", + // Different Go versions produce different results. + want: `ok %!s\(PANIC=(String method: )?panic\)/ok %!s\(PANIC=(String method: )?panic\)`, + regexp: true, + }, { + err: fmtTwice("%o %s", panicValue{}, "ok"), + fmt: "%s", + want: "{} ok/{} ok", + }, { + err: adapted{"adapted", nil}, + fmt: "%+v", + want: "adapted:" + + "\n detail", + }, { + err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, + fmt: "%+v", + want: "outer:" + + "\n detail" + + "\n - mid:" + + "\n detail" + + "\n - inner:" + + "\n detail", + }} + 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) + var ok bool + if tc.regexp { + var err error + ok, err = regexp.MatchString(tc.want, got) + if err != nil { + t.Fatal(err) + } + } else { + ok = got == tc.want + } + if !ok { + t.Errorf("\n got: %q\nwant: %q", got, tc.want) + } + }) + } +} + +func TestAdaptor(t *testing.T) { + testCases := []struct { + err error + fmt string + want string + regexp bool + }{{ + err: adapted{"adapted", nil}, + fmt: "%+v", + want: "adapted:" + + "\n detail", + }, { + err: adapted{"outer", adapted{"mid", adapted{"inner", nil}}}, + fmt: "%+v", + want: "outer:" + + "\n detail" + + "\n - mid:" + + "\n detail" + + "\n - inner:" + + "\n detail", + }} + 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 _ xerrors.Formatter = wrapped{} + +type wrapped struct { + msg string + err error +} + +func (e wrapped) Error() string { return "should call Format" } + +func (e wrapped) Format(s fmt.State, verb rune) { + xerrors.FormatError(&e, s, verb) +} + +func (e wrapped) FormatError(p xerrors.Printer) (next error) { + p.Print(e.msg) + p.Detail() + p.Print("somefile.go:123") + return e.err +} + +var _ xerrors.Formatter = detailed{} + +type detailed struct{} + +func (e detailed) Error() string { return fmt.Sprint(e) } + +func (detailed) FormatError(p xerrors.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 +} + +type withFrameAndMore struct { + frame xerrors.Frame +} + +func (e *withFrameAndMore) Error() string { return fmt.Sprint(e) } + +func (e *withFrameAndMore) Format(s fmt.State, v rune) { + xerrors.FormatError(e, s, v) +} + +func (e *withFrameAndMore) FormatError(p xerrors.Printer) (next error) { + p.Print("something") + if p.Detail() { + e.frame.Format(p) + p.Print("something more") + } + return nil +} + +type spurious string + +func (e spurious) Error() string { return fmt.Sprint(e) } + +// move to 1_12 test file +func (e spurious) Format(s fmt.State, verb rune) { + xerrors.FormatError(e, s, verb) +} + +func (e spurious) FormatError(p xerrors.Printer) (next error) { + p.Print("spurious") + p.Detail() // Call detail even if we don't print anything + if e == "" { + p.Print() + } else { + p.Print("\n", string(e)) // print extraneous leading newline + } + return nil +} + +type adapted struct { + msg string + err error +} + +func (e adapted) Error() string { return string(e.msg) } + +func (e adapted) Format(s fmt.State, verb rune) { + xerrors.FormatError(e, s, verb) +} + +func (e adapted) FormatError(p xerrors.Printer) error { + p.Print(e.msg) + p.Detail() + p.Print("detail") + return e.err +} + +// formatError is an error implementing Format instead of xerrors.Formatter. +// The implementation mimics the implementation of github.com/pkg/errors. +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(s fmt.State, verb rune) { + xerrors.FormatError(e, s, verb) +} + +func (e fmtTwiceErr) FormatError(p xerrors.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") } + +var rePath = regexp.MustCompile(`( [^ ]*)xerrors.*test\.`) +var reLine = regexp.MustCompile(":[0-9]*\n?$") + +func cleanPath(s string) string { + s = rePath.ReplaceAllString(s, "/path.") + s = reLine.ReplaceAllString(s, ":xxx") + s = strings.Replace(s, "\n ", "", -1) + s = strings.Replace(s, " /", "/", -1) + return s +} + +func errToParts(err error) (a []string) { + for err != nil { + var p testPrinter + if xerrors.Unwrap(err) != nil { + p.str += "wraps:" + } + f, ok := err.(xerrors.Formatter) + if !ok { + a = append(a, err.Error()) + break + } + err = f.FormatError(&p) + a = append(a, cleanPath(p.str)) + } + return a + +} + +type testPrinter struct { + str string +} + +func (p *testPrinter) Print(a ...interface{}) { + p.str += fmt.Sprint(a...) +} + +func (p *testPrinter) Printf(format string, a ...interface{}) { + p.str += fmt.Sprintf(format, a...) +} + +func (p *testPrinter) Detail() bool { + p.str += " /" + return true +}
diff --git a/format.go b/format.go new file mode 100644 index 0000000..1bc9c26 --- /dev/null +++ b/format.go
@@ -0,0 +1,34 @@ +// 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 xerrors + +// A Formatter formats error messages. +type Formatter interface { + error + + // FormatError prints the receiver's first error and returns the next error in + // the error chain, if any. + FormatError(p Printer) (next error) +} + +// A Printer formats error messages. +// +// The most common implementation of Printer is the one provided by package fmt +// during Printf (as of Go 1.13). Localization packages such as golang.org/x/text/message +// typically provide their own implementations. +type Printer interface { + // Print appends args to the message output. + Print(args ...interface{}) + + // Printf writes a formatted string. + Printf(format string, args ...interface{}) + + // Detail reports whether error detail is requested. + // After the first call to Detail, all text written to the Printer + // is formatted as additional detail, or ignored when + // detail has not been requested. + // If Detail returns false, the caller can avoid printing the detail at all. + Detail() bool +}
diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..0de628e --- /dev/null +++ b/frame.go
@@ -0,0 +1,56 @@ +// 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 xerrors + +import ( + "runtime" +) + +// A Frame contains part of a call stack. +type Frame struct { + // Make room for three PCs: the one we were asked for, what it called, + // and possibly a PC for skipPleaseUseCallersFrames. See: + // https://go.googlesource.com/go/+/032678e0fb/src/runtime/extern.go#169 + frames [3]uintptr +} + +// Caller returns a Frame that describes a frame on the caller's stack. +// The argument skip is the number of frames to skip over. +// Caller(0) returns the frame for the caller of Caller. +func Caller(skip int) Frame { + var s Frame + runtime.Callers(skip+1, s.frames[:]) + return s +} + +// location reports the file, line, and function of a frame. +// +// The returned function may be "" even if file and line are not. +func (f Frame) location() (function, file string, line int) { + frames := runtime.CallersFrames(f.frames[:]) + if _, ok := frames.Next(); !ok { + return "", "", 0 + } + fr, ok := frames.Next() + if !ok { + return "", "", 0 + } + return fr.Function, fr.File, fr.Line +} + +// Format prints the stack as error detail. +// It should be called from an error's Format implementation +// after printing any other error detail. +func (f Frame) Format(p Printer) { + if p.Detail() { + function, file, line := f.location() + if function != "" { + p.Printf("%s\n ", function) + } + if file != "" { + p.Printf("%s:%d\n", file, line) + } + } +}
diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eb71df3 --- /dev/null +++ b/go.mod
@@ -0,0 +1 @@ +module golang.org/x/xerrors
diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 0000000..89f4eca --- /dev/null +++ b/internal/internal.go
@@ -0,0 +1,8 @@ +// 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 internal + +// EnableTrace indicates whether stack information should be recorded in errors. +var EnableTrace = true
diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..e13f319 --- /dev/null +++ b/stack_test.go
@@ -0,0 +1,60 @@ +// 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 xerrors_test + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "golang.org/x/xerrors" + "golang.org/x/xerrors/internal" +) + +type myType struct{} + +func (myType) Format(s fmt.State, v rune) { + s.Write(bytes.Repeat([]byte("Hi! "), 10)) +} + +func BenchmarkErrorf(b *testing.B) { + err := xerrors.New("foo") + // pi := big.NewFloat(3.14) // Something expensive. + num := big.NewInt(5) + args := func(a ...interface{}) []interface{} { return a } + benchCases := []struct { + name string + format string + args []interface{} + }{ + {"no_format", "msg: %v", args(err)}, + {"with_format", "failed %d times: %v", args(5, err)}, + {"method: mytype", "pi: %v", args("myfile.go", myType{}, err)}, + {"method: number", "pi: %v", args("myfile.go", num, err)}, + } + for _, bc := range benchCases { + b.Run(bc.name, func(b *testing.B) { + b.Run("ExpWithTrace", func(b *testing.B) { + for i := 0; i < b.N; i++ { + xerrors.Errorf(bc.format, bc.args...) + } + }) + b.Run("ExpNoTrace", func(b *testing.B) { + internal.EnableTrace = false + defer func() { internal.EnableTrace = true }() + + for i := 0; i < b.N; i++ { + xerrors.Errorf(bc.format, bc.args...) + } + }) + b.Run("Core", func(b *testing.B) { + for i := 0; i < b.N; i++ { + fmt.Errorf(bc.format, bc.args...) + } + }) + }) + } +}
diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..eee732b --- /dev/null +++ b/wrap.go
@@ -0,0 +1,98 @@ +// 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 xerrors + +import ( + "reflect" +) + +// A Wrapper provides context around another error. +type Wrapper interface { + // Unwrap returns the next error in the error chain. + // If there is no next error, Unwrap returns nil. + Unwrap() error +} + +// Opaque returns an error with the same error formatting as err +// but that does not match err and cannot be unwrapped. +func Opaque(err error) error { + return noWrapper{err} +} + +type noWrapper struct { + error +} + +func (e noWrapper) FormatError(p Printer) (next error) { + if f, ok := e.error.(Formatter); ok { + return f.FormatError(p) + } + p.Print(e.error) + return nil +} + +// Unwrap returns the result of calling the Unwrap method on err, if err implements +// Unwrap. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + u, ok := err.(Wrapper) + if !ok { + return nil + } + return u.Unwrap() +} + +// Is reports whether any error in err's chain matches target. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +func Is(err, target error) bool { + if target == nil { + return err == target + } + for { + if err == target { + return true + } + if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { + return true + } + // TODO: consider supporing target.Is(err). This would allow + // user-definable predicates, but also may allow for coping with sloppy + // APIs, thereby making it easier to get away with them. + if err = Unwrap(err); err == nil { + return false + } + } +} + +// As finds the first error in err's chain that matches the type to which target +// points, and if so, sets the target to its value and and returns true. An error +// matches a type if it is of the same type, or if it has a method As(interface{}) bool +// such that As(target) returns true. As will panic if target is nil or not a pointer. +// +// The As method should set the target to its value and return true if err +// matches the type to which target points. +func As(err error, target interface{}) bool { + if target == nil { + panic("errors: target cannot be nil") + } + typ := reflect.TypeOf(target) + if typ.Kind() != reflect.Ptr { + panic("errors: target must be a pointer") + } + targetType := typ.Elem() + for { + if reflect.TypeOf(err) == targetType { + reflect.ValueOf(target).Elem().Set(reflect.ValueOf(err)) + return true + } + if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) { + return true + } + if err = Unwrap(err); err == nil { + return false + } + } +}
diff --git a/wrap_test.go b/wrap_test.go new file mode 100644 index 0000000..677b209 --- /dev/null +++ b/wrap_test.go
@@ -0,0 +1,202 @@ +// 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 xerrors_test + +import ( + "fmt" + "os" + "testing" + + "golang.org/x/xerrors" +) + +func TestIs(t *testing.T) { + err1 := xerrors.New("1") + erra := xerrors.Errorf("wrap 2: %w", err1) + errb := xerrors.Errorf("wrap 3: %w", erra) + erro := xerrors.Opaque(err1) + errco := xerrors.Errorf("opaque: %w", erro) + err3 := xerrors.New("3") + + poser := &poser{"either 1 or 3", func(err error) bool { + return err == err1 || err == err3 + }} + + testCases := []struct { + err error + target error + match bool + }{ + {nil, nil, true}, + {err1, nil, false}, + {err1, err1, true}, + {erra, err1, true}, + {errb, err1, true}, + {errco, erro, true}, + {errco, err1, false}, + {erro, erro, true}, + {err1, err3, false}, + {erra, err3, false}, + {errb, err3, false}, + {poser, err1, true}, + {poser, err3, true}, + {poser, erra, false}, + {poser, errb, false}, + {poser, erro, false}, + {poser, errco, false}, + } + for _, tc := range testCases { + t.Run("", func(t *testing.T) { + if got := xerrors.Is(tc.err, tc.target); got != tc.match { + t.Errorf("Is(%v, %v) = %v, want %v", tc.err, tc.target, got, tc.match) + } + }) + } +} + +type poser struct { + msg string + f func(error) bool +} + +func (p *poser) Error() string { return p.msg } +func (p *poser) Is(err error) bool { return p.f(err) } +func (p *poser) As(err interface{}) bool { + switch x := err.(type) { + case **poser: + *x = p + case *errorT: + *x = errorT{} + case **os.PathError: + *x = &os.PathError{} + default: + return false + } + return true +} + +func TestAs(t *testing.T) { + var errT errorT + var errP *os.PathError + var p *poser + _, errF := os.Open("non-existing") + + testCases := []struct { + err error + target interface{} + match bool + }{{ + xerrors.Errorf("pittied the fool: %w", errorT{}), + &errT, + true, + }, { + errF, + &errP, + true, + }, { + xerrors.Opaque(errT), + &errT, + false, + }, { + errorT{}, + &errP, + false, + }, { + errWrap{nil}, + &errT, + false, + }, { + &poser{"error", nil}, + &errT, + true, + }, { + &poser{"path", nil}, + &errP, + true, + }, { + &poser{"oh no", nil}, + &p, + true, + }, { + &poser{"oo", nil}, + &errF, + false, + }} + for _, tc := range testCases { + name := fmt.Sprintf("As(Errorf(..., %v), %v)", tc.err, tc.target) + t.Run(name, func(t *testing.T) { + match := xerrors.As(tc.err, tc.target) + if match != tc.match { + t.Fatalf("match: got %v; want %v", match, tc.match) + } + if !match { + return + } + if tc.target == nil { + t.Fatalf("non-nil result after match") + } + }) + } +} + +func TestUnwrap(t *testing.T) { + err1 := xerrors.New("1") + erra := xerrors.Errorf("wrap 2: %w", err1) + erro := xerrors.Opaque(err1) + + testCases := []struct { + err error + want error + }{ + {nil, nil}, + {errWrap{nil}, nil}, + {err1, nil}, + {erra, err1}, + {xerrors.Errorf("wrap 3: %w", erra), erra}, + + {erro, nil}, + {xerrors.Errorf("opaque: %w", erro), erro}, + } + for _, tc := range testCases { + if got := xerrors.Unwrap(tc.err); got != tc.want { + t.Errorf("Unwrap(%v) = %v, want %v", tc.err, got, tc.want) + } + } +} + +func TestOpaque(t *testing.T) { + got := fmt.Sprintf("%v", xerrors.Errorf("foo: %v", xerrors.Opaque(errorT{}))) + want := "foo: errorT" + if got != want { + t.Errorf("error without Format: got %v; want %v", got, want) + } + + got = fmt.Sprintf("%v", xerrors.Errorf("foo: %v", xerrors.Opaque(errorD{}))) + want = "foo: errorD" + if got != want { + t.Errorf("error with Format: got %v; want %v", got, want) + } +} + +type errorT struct{} + +func (errorT) Error() string { return "errorT" } + +type errorD struct{} + +func (errorD) Error() string { return "errorD" } + +func (errorD) FormatError(p xerrors.Printer) error { + p.Print("errorD") + p.Detail() + p.Print("detail") + return nil +} + +type errWrap struct{ error } + +func (errWrap) Error() string { return "wrapped" } + +func (errWrap) Unwrap() error { return nil }