Skip to content

Commit e37241e

Browse files
committed
feat: Add the -errorf-multi flag
This makes it possible to enforce multiple wraps only when desired. When still using Go 1.19, it is recommended to set this flag to false.
1 parent c5b61ac commit e37241e

File tree

4 files changed

+61
-21
lines changed

4 files changed

+61
-21
lines changed

errorlint/analysis.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ func NewAnalyzer() *analysis.Analyzer {
1919
}
2020

2121
var (
22-
flagSet flag.FlagSet
23-
checkComparison bool
24-
checkAsserts bool
25-
checkErrorf bool
22+
flagSet flag.FlagSet
23+
checkComparison bool
24+
checkAsserts bool
25+
checkErrorf bool
26+
checkErrorfMulti bool
2627
)
2728

2829
func init() {
2930
flagSet.BoolVar(&checkComparison, "comparison", true, "Check for plain error comparisons")
3031
flagSet.BoolVar(&checkAsserts, "asserts", true, "Check for plain type assertions and type switches")
3132
flagSet.BoolVar(&checkErrorf, "errorf", false, "Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats")
33+
flagSet.BoolVar(&checkErrorfMulti, "errorf-multi", true, "Permit more than 1 %w verb, valid per Go 1.20 (Requires -errorf=true)")
3234
}
3335

3436
func run(pass *analysis.Pass) (interface{}, error) {
@@ -43,7 +45,7 @@ func run(pass *analysis.Pass) (interface{}, error) {
4345
lints = append(lints, l...)
4446
}
4547
if checkErrorf {
46-
l := LintFmtErrorfCalls(pass.Fset, *pass.TypesInfo)
48+
l := LintFmtErrorfCalls(pass.Fset, *pass.TypesInfo, checkErrorfMulti)
4749
lints = append(lints, l...)
4850
}
4951
sort.Sort(ByPosition(lints))

errorlint/analysis_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ func TestErrorsIs(t *testing.T) {
1717

1818
func TestFmtErrorf(t *testing.T) {
1919
analyzer := NewAnalyzer()
20-
err := analyzer.Flags.Set("errorf", "true")
21-
if err != nil {
20+
if err := analyzer.Flags.Set("errorf", "true"); err != nil {
2221
log.Fatal(err)
2322
}
2423
analysistest.Run(t, analysistest.TestData(), analyzer, "fmterrorf")
2524
}
2625

26+
func TestFmtErrorfMultiple(t *testing.T) {
27+
analyzer := NewAnalyzer()
28+
if err := analyzer.Flags.Set("errorf", "true"); err != nil {
29+
log.Fatal(err)
30+
}
31+
if err := analyzer.Flags.Set("errorf-multi", "false"); err != nil {
32+
log.Fatal(err)
33+
}
34+
analysistest.Run(t, analysistest.TestData(), analyzer, "fmterrorf-go1.19")
35+
}
36+
2737
func TestAllowedComparisons(t *testing.T) {
2838
analyzer := NewAnalyzer()
2939
analysistest.Run(t, analysistest.TestData(), analyzer, "allowed")

errorlint/lint.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func (l ByPosition) Less(i, j int) bool {
2222
return l[i].Pos < l[j].Pos
2323
}
2424

25-
func LintFmtErrorfCalls(fset *token.FileSet, info types.Info) []Lint {
25+
func LintFmtErrorfCalls(fset *token.FileSet, info types.Info, multipleWraps bool) []Lint {
2626
lints := []Lint{}
2727
for expr, t := range info.Types {
2828
// Search for error expressions that are the result of fmt.Errorf
@@ -42,30 +42,38 @@ func LintFmtErrorfCalls(fset *token.FileSet, info types.Info) []Lint {
4242
}
4343

4444
// For any arguments that are errors, check whether the wrapping verb is used. %w may occur
45-
// for multiple errors in one Errorf invocation. We raise an issue if at least one error
46-
// does not have a corresponding wrapping verb.
47-
var lintArg ast.Expr
45+
// for multiple errors in one Errorf invocation, unless multipleWraps is true. We raise an
46+
// issue if at least one error does not have a corresponding wrapping verb.
4847
args := call.Args[1:]
48+
wrapCount := 0
4949
for i := 0; i < len(args) && i < len(formatVerbs); i++ {
5050
if !implementsError(info.Types[args[i]].Type) && !isErrorStringCall(info, args[i]) {
5151
continue
5252
}
53+
verb := formatVerbs[i]
5354

54-
if formatVerbs[i] == "w" {
55-
continue
55+
if verb == "w" {
56+
wrapCount++
57+
if multipleWraps {
58+
continue
59+
}
60+
if wrapCount > 1 {
61+
lints = append(lints, Lint{
62+
Message: "only one %w verb is permitted per format string",
63+
Pos: args[i].Pos(),
64+
})
65+
break
66+
}
5667
}
5768

58-
if lintArg == nil {
59-
lintArg = args[i]
69+
if multipleWraps && wrapCount == 0 {
70+
lints = append(lints, Lint{
71+
Message: "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors",
72+
Pos: args[i].Pos(),
73+
})
6074
break
6175
}
6276
}
63-
if lintArg != nil {
64-
lints = append(lints, Lint{
65-
Message: "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors",
66-
Pos: lintArg.Pos(),
67-
})
68-
}
6977
}
7078
return lints
7179
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package issues
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
func Single() error {
9+
err1 := errors.New("oops1")
10+
err2 := errors.New("oops2")
11+
err3 := errors.New("oops3")
12+
return fmt.Errorf("%w, %v, %v", err1, err2, err3)
13+
}
14+
15+
func Multiple() error {
16+
err1 := errors.New("oops1")
17+
err2 := errors.New("oops2")
18+
err3 := errors.New("oops3")
19+
return fmt.Errorf("%w, %w, %w", err1, err2, err3) // want "only one %w verb is permitted per format string"
20+
}

0 commit comments

Comments
 (0)