Skip to content

Commit eafcabc

Browse files
committed
message: add Errorf and %w support to formatter
Signed-off-by: Sam Whited <[email protected]>
1 parent 3043346 commit eafcabc

File tree

5 files changed

+235
-1
lines changed

5 files changed

+235
-1
lines changed

internal/format/errors_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package format_test
6+
7+
import (
8+
"errors"
9+
"slices"
10+
"testing"
11+
12+
"golang.org/x/text/internal/format"
13+
)
14+
15+
func TestErrorf(t *testing.T) {
16+
wrapped := errors.New("inner error")
17+
for _, test := range []struct {
18+
fmtStr string
19+
args []any
20+
wantWrapped []int
21+
}{
22+
0: {
23+
fmtStr: "%w",
24+
args: []any{wrapped},
25+
wantWrapped: []int{0},
26+
},
27+
1: {
28+
fmtStr: "%w %v%w",
29+
args: []any{wrapped, 1, wrapped},
30+
wantWrapped: []int{0, 2},
31+
},
32+
} {
33+
p := format.Parser{}
34+
p.Reset(test.args)
35+
p.SetFormat(test.fmtStr)
36+
for p.Scan() {
37+
}
38+
if slices.Compare(test.wantWrapped, p.WrappedErrs) != 0 {
39+
t.Errorf("wrong wrapped: got=%v, want=%v", p.WrappedErrs, test.wantWrapped)
40+
}
41+
}
42+
}

internal/format/parser.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type Parser struct {
4343
// goodArgNum records whether the most recent reordering directive was valid.
4444
goodArgNum bool
4545

46+
// WrappedErrs records the targets of the %w verb.
47+
WrappedErrs []int
48+
4649
// position info
4750
format string
4851
startPos int
@@ -55,6 +58,7 @@ func (p *Parser) Reset(args []interface{}) {
5558
p.Args = args
5659
p.ArgNum = 0
5760
p.startPos = 0
61+
p.WrappedErrs = p.WrappedErrs[:0]
5862
p.Reordered = false
5963
}
6064

@@ -148,7 +152,11 @@ simpleFormat:
148152
// Fast path for common case of ascii lower case simple verbs
149153
// without precision or width or argument indices.
150154
if 'a' <= c && c <= 'z' && p.ArgNum < len(p.Args) {
151-
if c == 'v' {
155+
switch c {
156+
case 'w':
157+
p.WrappedErrs = append(p.WrappedErrs, p.ArgNum)
158+
fallthrough
159+
case 'v':
152160
// Go syntax
153161
p.SharpV = p.Sharp
154162
p.Sharp = false
@@ -245,6 +253,9 @@ simpleFormat:
245253
case p.ArgNum >= len(p.Args): // No argument left over to print for the current verb.
246254
p.Status = StatusMissingArg
247255
p.ArgNum++
256+
case verb == 'w':
257+
p.WrappedErrs = append(p.WrappedErrs, p.ArgNum)
258+
fallthrough
248259
case verb == 'v':
249260
// Go syntax
250261
p.SharpV = p.Sharp

message/errrors_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package message_test
6+
7+
import (
8+
"errors"
9+
"reflect"
10+
"testing"
11+
12+
"golang.org/x/text/language"
13+
"golang.org/x/text/message"
14+
)
15+
16+
func TestErorrf(t *testing.T) {
17+
wrapped := errors.New("inner error")
18+
p := message.NewPrinter(language.Und)
19+
for _, test := range []struct {
20+
err error
21+
wantText string
22+
wantUnwrap error
23+
wantSplit []error
24+
}{{
25+
err: p.Errorf("%w", wrapped),
26+
wantText: "inner error",
27+
wantUnwrap: wrapped,
28+
}, {
29+
err: p.Errorf("added context: %w", wrapped),
30+
wantText: "added context: inner error",
31+
wantUnwrap: wrapped,
32+
}, {
33+
err: p.Errorf("%w with added context", wrapped),
34+
wantText: "inner error with added context",
35+
wantUnwrap: wrapped,
36+
}, {
37+
err: p.Errorf("%s %w %v", "prefix", wrapped, "suffix"),
38+
wantText: "prefix inner error suffix",
39+
wantUnwrap: wrapped,
40+
}, {
41+
err: p.Errorf("%[2]s: %[1]w", wrapped, "positional verb"),
42+
wantText: "positional verb: inner error",
43+
wantUnwrap: wrapped,
44+
}, {
45+
err: p.Errorf("%v", wrapped),
46+
wantText: "inner error",
47+
}, {
48+
err: p.Errorf("added context: %v", wrapped),
49+
wantText: "added context: inner error",
50+
}, {
51+
err: p.Errorf("%v with added context", wrapped),
52+
wantText: "inner error with added context",
53+
}, {
54+
err: p.Errorf("%w is not an error", "not-an-error"),
55+
wantText: "%!w(string=not-an-error) is not an error",
56+
}, {
57+
err: p.Errorf("wrapped two errors: %w %w", errString("1"), errString("2")),
58+
wantText: "wrapped two errors: 1 2",
59+
wantSplit: []error{errString("1"), errString("2")},
60+
}, {
61+
err: p.Errorf("wrapped three errors: %w %w %w", errString("1"), errString("2"), errString("3")),
62+
wantText: "wrapped three errors: 1 2 3",
63+
wantSplit: []error{errString("1"), errString("2"), errString("3")},
64+
}, {
65+
err: p.Errorf("wrapped nil error: %w %w %w", errString("1"), nil, errString("2")),
66+
wantText: "wrapped nil error: 1 %!w(<nil>) 2",
67+
wantSplit: []error{errString("1"), errString("2")},
68+
}, {
69+
err: p.Errorf("wrapped one non-error: %w %w %w", errString("1"), "not-an-error", errString("3")),
70+
wantText: "wrapped one non-error: 1 %!w(string=not-an-error) 3",
71+
wantSplit: []error{errString("1"), errString("3")},
72+
}, {
73+
err: p.Errorf("wrapped errors out of order: %[3]w %[2]w %[1]w", errString("1"), errString("2"), errString("3")),
74+
wantText: "wrapped errors out of order: 3 2 1",
75+
wantSplit: []error{errString("1"), errString("2"), errString("3")},
76+
}, {
77+
err: p.Errorf("wrapped several times: %[1]w %[1]w %[2]w %[1]w", errString("1"), errString("2")),
78+
wantText: "wrapped several times: 1 1 2 1",
79+
wantSplit: []error{errString("1"), errString("2")},
80+
}, {
81+
err: p.Errorf("%w", nil),
82+
wantText: "%!w(<nil>)",
83+
wantUnwrap: nil, // still nil
84+
}} {
85+
if got, want := errors.Unwrap(test.err), test.wantUnwrap; got != want {
86+
t.Errorf("Formatted error: %v\nerrors.Unwrap() = %v, want %v", test.err, got, want)
87+
}
88+
if got, want := splitErr(test.err), test.wantSplit; !reflect.DeepEqual(got, want) {
89+
t.Errorf("Formatted error: %v\nUnwrap() []error = %v, want %v", test.err, got, want)
90+
}
91+
if got, want := test.err.Error(), test.wantText; got != want {
92+
t.Errorf("err.Error() = %q, want %q", got, want)
93+
}
94+
}
95+
}
96+
97+
func splitErr(err error) []error {
98+
if e, ok := err.(interface{ Unwrap() []error }); ok {
99+
return e.Unwrap()
100+
}
101+
return nil
102+
}
103+
104+
type errString string
105+
106+
func (e errString) Error() string { return string(e) }

message/message.go

+61
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package message // import "golang.org/x/text/message"
66

77
import (
8+
"errors"
89
"io"
910
"os"
11+
"slices"
1012

1113
// Include features to facilitate generated catalogs.
1214
_ "golang.org/x/text/feature/plural"
@@ -136,6 +138,65 @@ func (p *Printer) Printf(key Reference, a ...interface{}) (n int, err error) {
136138
return n, err
137139
}
138140

141+
// Errorf is like fmt.Errorf, but using language-specific formatting.
142+
func (p *Printer) Errorf(key Reference, a ...interface{}) error {
143+
pp := newPrinter(p)
144+
pp.wrapErrs = true
145+
lookupAndFormat(pp, key, a)
146+
s := pp.String()
147+
var err error
148+
switch len(pp.fmt.WrappedErrs) {
149+
case 0:
150+
err = errors.New(s)
151+
case 1:
152+
w := &wrapError{msg: s}
153+
w.err, _ = a[pp.fmt.WrappedErrs[0]].(error)
154+
err = w
155+
default:
156+
if pp.fmt.Reordered {
157+
slices.Sort(pp.fmt.WrappedErrs)
158+
}
159+
var errs []error
160+
for i, argNum := range pp.fmt.WrappedErrs {
161+
if i > 0 && pp.fmt.WrappedErrs[i-1] == argNum {
162+
continue
163+
}
164+
if e, ok := a[argNum].(error); ok {
165+
errs = append(errs, e)
166+
}
167+
}
168+
err = &wrapErrors{s, errs}
169+
}
170+
pp.free()
171+
return err
172+
}
173+
174+
type wrapError struct {
175+
msg string
176+
err error
177+
}
178+
179+
func (e *wrapError) Error() string {
180+
return e.msg
181+
}
182+
183+
func (e *wrapError) Unwrap() error {
184+
return e.err
185+
}
186+
187+
type wrapErrors struct {
188+
msg string
189+
errs []error
190+
}
191+
192+
func (e *wrapErrors) Error() string {
193+
return e.msg
194+
}
195+
196+
func (e *wrapErrors) Unwrap() []error {
197+
return e.errs
198+
}
199+
139200
func lookupAndFormat(p *printer, r Reference, a []interface{}) {
140201
p.fmt.Reset(a)
141202
switch v := r.(type) {

message/print.go

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func (p *printer) free() {
6060
p.Buffer.Reset()
6161
p.arg = nil
6262
p.value = reflect.Value{}
63+
p.fmt.WrappedErrs = p.fmt.WrappedErrs[:0]
6364
printerPool.Put(p)
6465
}
6566

@@ -82,6 +83,9 @@ type printer struct {
8283
// fmt is used to format basic items such as integers or strings.
8384
fmt formatInfo
8485

86+
// wrapErrs is set when the format string may contain a %w verb.
87+
wrapErrs bool
88+
8589
// panicking is set by catchPanic to avoid infinite panic, recover, panic, ... recursion.
8690
panicking bool
8791
// 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) {
594598
if p.erroring {
595599
return
596600
}
601+
if verb == 'w' {
602+
// It is invalid to use %w other than with Errorf or with a non-error arg.
603+
_, ok := p.arg.(error)
604+
if !ok || !p.wrapErrs {
605+
p.badVerb(verb)
606+
return true
607+
}
608+
// If the arg is an error, pass 'v' as the verb to it.
609+
verb = 'v'
610+
}
597611
// Is it a Formatter?
598612
if formatter, ok := p.arg.(format.Formatter); ok {
599613
handled = true

0 commit comments

Comments
 (0)