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 <dneil@google.com>
diff --git a/fmt.go b/fmt.go
index 74c1c93..829862d 100644
--- a/fmt.go
+++ b/fmt.go
@@ -7,10 +7,14 @@
 import (
 	"fmt"
 	"strings"
+	"unicode"
+	"unicode/utf8"
 
 	"golang.org/x/xerrors/internal"
 )
 
+const percentBangString = "%!"
+
 // Errorf formats according to a format specifier and returns the string as a
 // value that satisfies error.
 //
@@ -18,29 +22,71 @@
 // 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.
+// format string ends with ": %w", the returned error implements an Unwrap
+// method returning it.
+//
+// If the format specifier includes a %w verb with an error operand in a
+// position other than at the end, the returned error will still implement an
+// Unwrap method returning the operand, but the error's Format method will not
+// return the wrapped error.
+//
+// It is invalid to include more than one %w verb or to supply it with an
+// operand that does not implement the error interface. The %w verb is otherwise
+// a synonym for %v.
 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)}
+	// Support a ": %[wsv]" suffix, which works well with xerrors.Formatter.
+	wrap := strings.HasSuffix(format, ": %w")
+	idx, format2, ok := parsePercentW(format)
+	percentWElsewhere := !wrap && idx >= 0
+	if !percentWElsewhere && (wrap || strings.HasSuffix(format, ": %s") || strings.HasSuffix(format, ": %v")) {
+		err := errorAt(a, len(a)-1)
+		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}
 	}
-
-	// 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]...)
+	// Support %w anywhere.
+	// TODO: don't repeat the wrapped error's message when %w occurs in the middle.
+	msg := fmt.Sprintf(format2, a...)
+	if idx < 0 {
+		return &noWrapError{msg, nil, Caller(1)}
+	}
+	err := errorAt(a, idx)
+	if !ok || err == nil {
+		// Too many %ws or argument of %w is not an error. Approximate the Go
+		// 1.13 fmt.Errorf message.
+		return &noWrapError{fmt.Sprintf("%sw(%s)", percentBangString, msg), nil, Caller(1)}
+	}
 	frame := Frame{}
 	if internal.EnableTrace {
 		frame = Caller(1)
 	}
-	if wrap {
-		return &wrapError{msg, err, frame}
+	return &wrapError{msg, err, frame}
+}
+
+func errorAt(args []interface{}, i int) error {
+	if i < 0 || i >= len(args) {
+		return nil
 	}
-	return &noWrapError{msg, err, frame}
+	err, ok := args[i].(error)
+	if !ok {
+		return nil
+	}
+	return err
 }
 
 // formatPlusW is used to avoid the vet check that will barf at %w.
@@ -48,24 +94,56 @@
 	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
+// Return the index of the only %w in format, or -1 if none.
+// Also return a rewritten format string with %w replaced by %v, and
+// false if there is more than one %w.
+// TODO: handle "%[N]w".
+func parsePercentW(format string) (idx int, newFormat string, ok bool) {
+	// Loosely copied from golang.org/x/tools/go/analysis/passes/printf/printf.go.
+	idx = -1
+	ok = true
+	n := 0
+	sz := 0
+	var isW bool
+	for i := 0; i < len(format); i += sz {
+		if format[i] != '%' {
+			sz = 1
+			continue
+		}
+		// "%%" is not a format directive.
+		if i+1 < len(format) && format[i+1] == '%' {
+			sz = 2
+			continue
+		}
+		sz, isW = parsePrintfVerb(format[i:])
+		if isW {
+			if idx >= 0 {
+				ok = false
+			} else {
+				idx = n
+			}
+			// "Replace" the last character, the 'w', with a 'v'.
+			p := i + sz - 1
+			format = format[:p] + "v" + format[p+1:]
+		}
+		n++
 	}
+	return idx, format, ok
+}
 
-	if len(a) == 0 {
-		return nil, false
+// Parse the printf verb starting with a % at s[0].
+// Return how many bytes it occupies and whether the verb is 'w'.
+func parsePrintfVerb(s string) (int, bool) {
+	// Assume only that the directive is a sequence of non-letters followed by a single letter.
+	sz := 0
+	var r rune
+	for i := 1; i < len(s); i += sz {
+		r, sz = utf8.DecodeRuneInString(s[i:])
+		if unicode.IsLetter(r) {
+			return i + sz, r == 'w'
+		}
 	}
-
-	err, ok := a[len(a)-1].(error)
-	if !ok {
-		return nil, false
-	}
-
-	return err, wrap
+	return len(s), false
 }
 
 type noWrapError struct {
diff --git a/fmt_test.go b/fmt_test.go
index 6744b8a..7bf96a7 100644
--- a/fmt_test.go
+++ b/fmt_test.go
@@ -62,6 +62,10 @@
 		chain("wraps:wrapw/path.TestErrorf/path.go:xxx",
 			"chained/somefile.go:xxx"),
 	}, {
+		xerrors.Errorf("wrapw %w middle", chained),
+		chain("wraps:wrapw chained middle/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"),
 	}}
@@ -156,7 +160,7 @@
 		fmt: "%+v",
 		want: "something:" +
 			"\n    golang.org/x/xerrors_test.TestErrorFormatter" +
-			"\n        .+/fmt_test.go:97" +
+			"\n        .+/fmt_test.go:101" +
 			"\n    something more",
 		regexp: true,
 	}, {
diff --git a/fmt_unexported_test.go b/fmt_unexported_test.go
new file mode 100644
index 0000000..3affcae
--- /dev/null
+++ b/fmt_unexported_test.go
@@ -0,0 +1,51 @@
+// 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 "testing"
+
+func TestParsePrintfVerb(t *testing.T) {
+	for _, test := range []struct {
+		in       string
+		wantSize int
+		wantW    bool
+	}{
+		{"", 0, false},
+		{"%", 1, false},
+		{"%3.1", 4, false},
+		{"%w", 2, true},
+		{"%v", 2, false},
+		{"%3.*[4]d", 8, false},
+	} {
+		gotSize, gotW := parsePrintfVerb(test.in)
+		if gotSize != test.wantSize || gotW != test.wantW {
+			t.Errorf("parsePrintfVerb(%q) = (%d, %t), want (%d, %t)",
+				test.in, gotSize, gotW, test.wantSize, test.wantW)
+		}
+	}
+}
+
+func TestParsePercentW(t *testing.T) {
+	for _, test := range []struct {
+		in         string
+		wantIdx    int
+		wantFormat string
+		wantOK     bool
+	}{
+		{"", -1, "", true},
+		{"%", -1, "%", true},
+		{"%w", 0, "%v", true},
+		{"%w%w", 0, "%v%v", false},
+		{"%3.2s %+q %% %w %#v", 2, "%3.2s %+q %% %v %#v", true},
+		{"%3.2s %w %% %w %#v", 1, "%3.2s %v %% %v %#v", false},
+	} {
+		gotIdx, gotFormat, gotOK := parsePercentW(test.in)
+		if gotIdx != test.wantIdx || gotFormat != test.wantFormat || gotOK != test.wantOK {
+			t.Errorf("parsePercentW(%q) = (%d, %q, %t), want (%d, %q, %t)",
+				test.in, gotIdx, gotFormat, gotOK, test.wantIdx, test.wantFormat, test.wantOK)
+
+		}
+	}
+}
diff --git a/wrap_113_test.go b/wrap_113_test.go
new file mode 100644
index 0000000..25c3d80
--- /dev/null
+++ b/wrap_113_test.go
@@ -0,0 +1,28 @@
+// 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.
+
+//+build go1.13
+
+package xerrors_test
+
+import (
+	"errors"
+	"testing"
+
+	"golang.org/x/xerrors"
+)
+
+func TestErrorsIs(t *testing.T) {
+	var errSentinel = errors.New("sentinel")
+
+	got := errors.Is(xerrors.Errorf("%w", errSentinel), errSentinel)
+	if !got {
+		t.Error("got false, want true")
+	}
+
+	got = errors.Is(xerrors.Errorf("%w: %s", errSentinel, "foo"), errSentinel)
+	if !got {
+		t.Error("got false, want true")
+	}
+}