diff --git a/internal/format/errors_test.go b/internal/format/errors_test.go new file mode 100644 index 000000000..61a07a7df --- /dev/null +++ b/internal/format/errors_test.go @@ -0,0 +1,42 @@ +// Copyright 2024 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 format_test + +import ( + "errors" + "slices" + "testing" + + "golang.org/x/text/internal/format" +) + +func TestErrorf(t *testing.T) { + wrapped := errors.New("inner error") + for _, test := range []struct { + fmtStr string + args []any + wantWrapped []int + }{ + 0: { + fmtStr: "%w", + args: []any{wrapped}, + wantWrapped: []int{0}, + }, + 1: { + fmtStr: "%w %v%w", + args: []any{wrapped, 1, wrapped}, + wantWrapped: []int{0, 2}, + }, + } { + p := format.Parser{} + p.Reset(test.args) + p.SetFormat(test.fmtStr) + for p.Scan() { + } + if slices.Compare(test.wantWrapped, p.WrappedErrs) != 0 { + t.Errorf("wrong wrapped: got=%v, want=%v", p.WrappedErrs, test.wantWrapped) + } + } +} diff --git a/internal/format/parser.go b/internal/format/parser.go index 855aed71d..ac3e1315e 100644 --- a/internal/format/parser.go +++ b/internal/format/parser.go @@ -43,6 +43,9 @@ type Parser struct { // goodArgNum records whether the most recent reordering directive was valid. goodArgNum bool + // WrappedErrs records the targets of the %w verb. + WrappedErrs []int + // position info format string startPos int @@ -55,6 +58,7 @@ func (p *Parser) Reset(args []interface{}) { p.Args = args p.ArgNum = 0 p.startPos = 0 + p.WrappedErrs = p.WrappedErrs[:0] p.Reordered = false } @@ -148,7 +152,11 @@ simpleFormat: // Fast path for common case of ascii lower case simple verbs // without precision or width or argument indices. if 'a' <= c && c <= 'z' && p.ArgNum < len(p.Args) { - if c == 'v' { + switch c { + case 'w': + p.WrappedErrs = append(p.WrappedErrs, p.ArgNum) + fallthrough + case 'v': // Go syntax p.SharpV = p.Sharp p.Sharp = false @@ -245,6 +253,9 @@ simpleFormat: case p.ArgNum >= len(p.Args): // No argument left over to print for the current verb. p.Status = StatusMissingArg p.ArgNum++ + case verb == 'w': + p.WrappedErrs = append(p.WrappedErrs, p.ArgNum) + fallthrough case verb == 'v': // Go syntax p.SharpV = p.Sharp diff --git a/message/errrors_test.go b/message/errrors_test.go new file mode 100644 index 000000000..0f915ecc6 --- /dev/null +++ b/message/errrors_test.go @@ -0,0 +1,106 @@ +// Copyright 2024 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 message_test + +import ( + "errors" + "reflect" + "testing" + + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +func TestErorrf(t *testing.T) { + wrapped := errors.New("inner error") + p := message.NewPrinter(language.Und) + for _, test := range []struct { + err error + wantText string + wantUnwrap error + wantSplit []error + }{{ + err: p.Errorf("%w", wrapped), + wantText: "inner error", + wantUnwrap: wrapped, + }, { + err: p.Errorf("added context: %w", wrapped), + wantText: "added context: inner error", + wantUnwrap: wrapped, + }, { + err: p.Errorf("%w with added context", wrapped), + wantText: "inner error with added context", + wantUnwrap: wrapped, + }, { + err: p.Errorf("%s %w %v", "prefix", wrapped, "suffix"), + wantText: "prefix inner error suffix", + wantUnwrap: wrapped, + }, { + err: p.Errorf("%[2]s: %[1]w", wrapped, "positional verb"), + wantText: "positional verb: inner error", + wantUnwrap: wrapped, + }, { + err: p.Errorf("%v", wrapped), + wantText: "inner error", + }, { + err: p.Errorf("added context: %v", wrapped), + wantText: "added context: inner error", + }, { + err: p.Errorf("%v with added context", wrapped), + wantText: "inner error with added context", + }, { + err: p.Errorf("%w is not an error", "not-an-error"), + wantText: "%!w(string=not-an-error) is not an error", + }, { + err: p.Errorf("wrapped two errors: %w %w", errString("1"), errString("2")), + wantText: "wrapped two errors: 1 2", + wantSplit: []error{errString("1"), errString("2")}, + }, { + err: p.Errorf("wrapped three errors: %w %w %w", errString("1"), errString("2"), errString("3")), + wantText: "wrapped three errors: 1 2 3", + wantSplit: []error{errString("1"), errString("2"), errString("3")}, + }, { + err: p.Errorf("wrapped nil error: %w %w %w", errString("1"), nil, errString("2")), + wantText: "wrapped nil error: 1 %!w() 2", + wantSplit: []error{errString("1"), errString("2")}, + }, { + err: p.Errorf("wrapped one non-error: %w %w %w", errString("1"), "not-an-error", errString("3")), + wantText: "wrapped one non-error: 1 %!w(string=not-an-error) 3", + wantSplit: []error{errString("1"), errString("3")}, + }, { + err: p.Errorf("wrapped errors out of order: %[3]w %[2]w %[1]w", errString("1"), errString("2"), errString("3")), + wantText: "wrapped errors out of order: 3 2 1", + wantSplit: []error{errString("1"), errString("2"), errString("3")}, + }, { + err: p.Errorf("wrapped several times: %[1]w %[1]w %[2]w %[1]w", errString("1"), errString("2")), + wantText: "wrapped several times: 1 1 2 1", + wantSplit: []error{errString("1"), errString("2")}, + }, { + err: p.Errorf("%w", nil), + wantText: "%!w()", + wantUnwrap: nil, // still nil + }} { + if got, want := errors.Unwrap(test.err), test.wantUnwrap; got != want { + t.Errorf("Formatted error: %v\nerrors.Unwrap() = %v, want %v", test.err, got, want) + } + if got, want := splitErr(test.err), test.wantSplit; !reflect.DeepEqual(got, want) { + t.Errorf("Formatted error: %v\nUnwrap() []error = %v, want %v", test.err, got, want) + } + if got, want := test.err.Error(), test.wantText; got != want { + t.Errorf("err.Error() = %q, want %q", got, want) + } + } +} + +func splitErr(err error) []error { + if e, ok := err.(interface{ Unwrap() []error }); ok { + return e.Unwrap() + } + return nil +} + +type errString string + +func (e errString) Error() string { return string(e) } diff --git a/message/message.go b/message/message.go index 91a972642..cac6c38ec 100644 --- a/message/message.go +++ b/message/message.go @@ -5,8 +5,10 @@ package message // import "golang.org/x/text/message" import ( + "errors" "io" "os" + "slices" // Include features to facilitate generated catalogs. _ "golang.org/x/text/feature/plural" @@ -136,6 +138,65 @@ func (p *Printer) Printf(key Reference, a ...interface{}) (n int, err error) { return n, err } +// Errorf is like fmt.Errorf, but using language-specific formatting. +func (p *Printer) Errorf(key Reference, a ...interface{}) error { + pp := newPrinter(p) + pp.wrapErrs = true + lookupAndFormat(pp, key, a) + s := pp.String() + var err error + switch len(pp.fmt.WrappedErrs) { + case 0: + err = errors.New(s) + case 1: + w := &wrapError{msg: s} + w.err, _ = a[pp.fmt.WrappedErrs[0]].(error) + err = w + default: + if pp.fmt.Reordered { + slices.Sort(pp.fmt.WrappedErrs) + } + var errs []error + for i, argNum := range pp.fmt.WrappedErrs { + if i > 0 && pp.fmt.WrappedErrs[i-1] == argNum { + continue + } + if e, ok := a[argNum].(error); ok { + errs = append(errs, e) + } + } + err = &wrapErrors{s, errs} + } + pp.free() + return err +} + +type wrapError struct { + msg string + err error +} + +func (e *wrapError) Error() string { + return e.msg +} + +func (e *wrapError) Unwrap() error { + return e.err +} + +type wrapErrors struct { + msg string + errs []error +} + +func (e *wrapErrors) Error() string { + return e.msg +} + +func (e *wrapErrors) Unwrap() []error { + return e.errs +} + func lookupAndFormat(p *printer, r Reference, a []interface{}) { p.fmt.Reset(a) switch v := r.(type) { diff --git a/message/print.go b/message/print.go index da304cc0e..08278c430 100644 --- a/message/print.go +++ b/message/print.go @@ -60,6 +60,7 @@ func (p *printer) free() { p.Buffer.Reset() p.arg = nil p.value = reflect.Value{} + p.fmt.WrappedErrs = p.fmt.WrappedErrs[:0] printerPool.Put(p) } @@ -82,6 +83,9 @@ type printer struct { // fmt is used to format basic items such as integers or strings. fmt formatInfo + // wrapErrs is set when the format string may contain a %w verb. + wrapErrs bool + // panicking is set by catchPanic to avoid infinite panic, recover, panic, ... recursion. panicking bool // erroring is set when printing an error string to guard against calling handleMethods. @@ -594,6 +598,16 @@ func (p *printer) handleMethods(verb rune) (handled bool) { if p.erroring { return } + if verb == 'w' { + // It is invalid to use %w other than with Errorf or with a non-error arg. + _, ok := p.arg.(error) + if !ok || !p.wrapErrs { + p.badVerb(verb) + return true + } + // If the arg is an error, pass 'v' as the verb to it. + verb = 'v' + } // Is it a Formatter? if formatter, ok := p.arg.(format.Formatter); ok { handled = true