Skip to content

Commit 9bdfabe

Browse files
committed
Errorf: support %w anywhere in the format string
Allow a single %w to appear anywhere in the format string, matched by an operand that satisfies the error interface. This is complicated by a few things: - We don't want to do full-fledged printf verb parsing. We approximate, and do not handle argument indexes on %w like "%[3]w". - There is a messy interaction with the xerrors formatting system (not adopted in Go 1.13). When the %w is at the end we can control the error string so that the wrapped error's message is formatted properly, after the wrapping errors' message. With a %w in the middle, we can't do that. In this CL, we print the wrapped error's message twice: once in its place in the format string, and then again because it's wrapped. Fixes #go/33143. Change-Id: I6192096521664476e82d5a54b80e352e47397cfe Reviewed-on: https://go-review.googlesource.com/c/xerrors/+/186957 Reviewed-by: Damien Neil <[email protected]>
1 parent 1b5146a commit 9bdfabe

File tree

4 files changed

+192
-31
lines changed

4 files changed

+192
-31
lines changed

fmt.go

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,143 @@ package xerrors
77
import (
88
"fmt"
99
"strings"
10+
"unicode"
11+
"unicode/utf8"
1012

1113
"golang.org/x/xerrors/internal"
1214
)
1315

16+
const percentBangString = "%!"
17+
1418
// Errorf formats according to a format specifier and returns the string as a
1519
// value that satisfies error.
1620
//
1721
// The returned error includes the file and line number of the caller when
1822
// formatted with additional detail enabled. If the last argument is an error
1923
// the returned error's Format method will return it if the format string ends
2024
// with ": %s", ": %v", or ": %w". If the last argument is an error and the
21-
// format string ends with ": %w", the returned error implements Wrapper
22-
// with an Unwrap method returning it.
25+
// format string ends with ": %w", the returned error implements an Unwrap
26+
// method returning it.
27+
//
28+
// If the format specifier includes a %w verb with an error operand in a
29+
// position other than at the end, the returned error will still implement an
30+
// Unwrap method returning the operand, but the error's Format method will not
31+
// return the wrapped error.
32+
//
33+
// It is invalid to include more than one %w verb or to supply it with an
34+
// operand that does not implement the error interface. The %w verb is otherwise
35+
// a synonym for %v.
2336
func Errorf(format string, a ...interface{}) error {
24-
err, wrap := lastError(format, a)
2537
format = formatPlusW(format)
26-
if err == nil {
27-
return &noWrapError{fmt.Sprintf(format, a...), nil, Caller(1)}
38+
// Support a ": %[wsv]" suffix, which works well with xerrors.Formatter.
39+
wrap := strings.HasSuffix(format, ": %w")
40+
idx, format2, ok := parsePercentW(format)
41+
percentWElsewhere := !wrap && idx >= 0
42+
if !percentWElsewhere && (wrap || strings.HasSuffix(format, ": %s") || strings.HasSuffix(format, ": %v")) {
43+
err := errorAt(a, len(a)-1)
44+
if err == nil {
45+
return &noWrapError{fmt.Sprintf(format, a...), nil, Caller(1)}
46+
}
47+
// TODO: this is not entirely correct. The error value could be
48+
// printed elsewhere in format if it mixes numbered with unnumbered
49+
// substitutions. With relatively small changes to doPrintf we can
50+
// have it optionally ignore extra arguments and pass the argument
51+
// list in its entirety.
52+
msg := fmt.Sprintf(format[:len(format)-len(": %s")], a[:len(a)-1]...)
53+
frame := Frame{}
54+
if internal.EnableTrace {
55+
frame = Caller(1)
56+
}
57+
if wrap {
58+
return &wrapError{msg, err, frame}
59+
}
60+
return &noWrapError{msg, err, frame}
61+
}
62+
// Support %w anywhere.
63+
// TODO: don't repeat the wrapped error's message when %w occurs in the middle.
64+
msg := fmt.Sprintf(format2, a...)
65+
if idx < 0 {
66+
return &noWrapError{msg, nil, Caller(1)}
67+
}
68+
err := errorAt(a, idx)
69+
if !ok || err == nil {
70+
// Too many %ws or argument of %w is not an error. Approximate the Go
71+
// 1.13 fmt.Errorf message.
72+
return &noWrapError{fmt.Sprintf("%sw(%s)", percentBangString, msg), nil, Caller(1)}
2873
}
29-
30-
// TODO: this is not entirely correct. The error value could be
31-
// printed elsewhere in format if it mixes numbered with unnumbered
32-
// substitutions. With relatively small changes to doPrintf we can
33-
// have it optionally ignore extra arguments and pass the argument
34-
// list in its entirety.
35-
msg := fmt.Sprintf(format[:len(format)-len(": %s")], a[:len(a)-1]...)
3674
frame := Frame{}
3775
if internal.EnableTrace {
3876
frame = Caller(1)
3977
}
40-
if wrap {
41-
return &wrapError{msg, err, frame}
78+
return &wrapError{msg, err, frame}
79+
}
80+
81+
func errorAt(args []interface{}, i int) error {
82+
if i < 0 || i >= len(args) {
83+
return nil
4284
}
43-
return &noWrapError{msg, err, frame}
85+
err, ok := args[i].(error)
86+
if !ok {
87+
return nil
88+
}
89+
return err
4490
}
4591

4692
// formatPlusW is used to avoid the vet check that will barf at %w.
4793
func formatPlusW(s string) string {
4894
return s
4995
}
5096

51-
func lastError(format string, a []interface{}) (err error, wrap bool) {
52-
wrap = strings.HasSuffix(format, ": %w")
53-
if !wrap &&
54-
!strings.HasSuffix(format, ": %s") &&
55-
!strings.HasSuffix(format, ": %v") {
56-
return nil, false
57-
}
58-
59-
if len(a) == 0 {
60-
return nil, false
97+
// Return the index of the only %w in format, or -1 if none.
98+
// Also return a rewritten format string with %w replaced by %v, and
99+
// false if there is more than one %w.
100+
// TODO: handle "%[N]w".
101+
func parsePercentW(format string) (idx int, newFormat string, ok bool) {
102+
// Loosely copied from golang.org/x/tools/go/analysis/passes/printf/printf.go.
103+
idx = -1
104+
ok = true
105+
n := 0
106+
sz := 0
107+
var isW bool
108+
for i := 0; i < len(format); i += sz {
109+
if format[i] != '%' {
110+
sz = 1
111+
continue
112+
}
113+
// "%%" is not a format directive.
114+
if i+1 < len(format) && format[i+1] == '%' {
115+
sz = 2
116+
continue
117+
}
118+
sz, isW = parsePrintfVerb(format[i:])
119+
if isW {
120+
if idx >= 0 {
121+
ok = false
122+
} else {
123+
idx = n
124+
}
125+
// "Replace" the last character, the 'w', with a 'v'.
126+
p := i + sz - 1
127+
format = format[:p] + "v" + format[p+1:]
128+
}
129+
n++
61130
}
131+
return idx, format, ok
132+
}
62133

63-
err, ok := a[len(a)-1].(error)
64-
if !ok {
65-
return nil, false
134+
// Parse the printf verb starting with a % at s[0].
135+
// Return how many bytes it occupies and whether the verb is 'w'.
136+
func parsePrintfVerb(s string) (int, bool) {
137+
// Assume only that the directive is a sequence of non-letters followed by a single letter.
138+
sz := 0
139+
var r rune
140+
for i := 1; i < len(s); i += sz {
141+
r, sz = utf8.DecodeRuneInString(s[i:])
142+
if unicode.IsLetter(r) {
143+
return i + sz, r == 'w'
144+
}
66145
}
67-
68-
return err, wrap
146+
return len(s), false
69147
}
70148

71149
type noWrapError struct {

fmt_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func TestErrorf(t *testing.T) {
6161
xerrors.Errorf("wrapw: %w", chained),
6262
chain("wraps:wrapw/path.TestErrorf/path.go:xxx",
6363
"chained/somefile.go:xxx"),
64+
}, {
65+
xerrors.Errorf("wrapw %w middle", chained),
66+
chain("wraps:wrapw chained middle/path.TestErrorf/path.go:xxx",
67+
"chained/somefile.go:xxx"),
6468
}, {
6569
xerrors.Errorf("not wrapped: %+v", chained),
6670
chain("not wrapped: chained: somefile.go:123/path.TestErrorf/path.go:xxx"),
@@ -156,7 +160,7 @@ func TestErrorFormatter(t *testing.T) {
156160
fmt: "%+v",
157161
want: "something:" +
158162
"\n golang.org/x/xerrors_test.TestErrorFormatter" +
159-
"\n .+/fmt_test.go:97" +
163+
"\n .+/fmt_test.go:101" +
160164
"\n something more",
161165
regexp: true,
162166
}, {

fmt_unexported_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2018 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 xerrors
6+
7+
import "testing"
8+
9+
func TestParsePrintfVerb(t *testing.T) {
10+
for _, test := range []struct {
11+
in string
12+
wantSize int
13+
wantW bool
14+
}{
15+
{"", 0, false},
16+
{"%", 1, false},
17+
{"%3.1", 4, false},
18+
{"%w", 2, true},
19+
{"%v", 2, false},
20+
{"%3.*[4]d", 8, false},
21+
} {
22+
gotSize, gotW := parsePrintfVerb(test.in)
23+
if gotSize != test.wantSize || gotW != test.wantW {
24+
t.Errorf("parsePrintfVerb(%q) = (%d, %t), want (%d, %t)",
25+
test.in, gotSize, gotW, test.wantSize, test.wantW)
26+
}
27+
}
28+
}
29+
30+
func TestParsePercentW(t *testing.T) {
31+
for _, test := range []struct {
32+
in string
33+
wantIdx int
34+
wantFormat string
35+
wantOK bool
36+
}{
37+
{"", -1, "", true},
38+
{"%", -1, "%", true},
39+
{"%w", 0, "%v", true},
40+
{"%w%w", 0, "%v%v", false},
41+
{"%3.2s %+q %% %w %#v", 2, "%3.2s %+q %% %v %#v", true},
42+
{"%3.2s %w %% %w %#v", 1, "%3.2s %v %% %v %#v", false},
43+
} {
44+
gotIdx, gotFormat, gotOK := parsePercentW(test.in)
45+
if gotIdx != test.wantIdx || gotFormat != test.wantFormat || gotOK != test.wantOK {
46+
t.Errorf("parsePercentW(%q) = (%d, %q, %t), want (%d, %q, %t)",
47+
test.in, gotIdx, gotFormat, gotOK, test.wantIdx, test.wantFormat, test.wantOK)
48+
49+
}
50+
}
51+
}

wrap_113_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019 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+
//+build go1.13
6+
7+
package xerrors_test
8+
9+
import (
10+
"errors"
11+
"testing"
12+
13+
"golang.org/x/xerrors"
14+
)
15+
16+
func TestErrorsIs(t *testing.T) {
17+
var errSentinel = errors.New("sentinel")
18+
19+
got := errors.Is(xerrors.Errorf("%w", errSentinel), errSentinel)
20+
if !got {
21+
t.Error("got false, want true")
22+
}
23+
24+
got = errors.Is(xerrors.Errorf("%w: %s", errSentinel, "foo"), errSentinel)
25+
if !got {
26+
t.Error("got false, want true")
27+
}
28+
}

0 commit comments

Comments
 (0)