├── go.mod ├── errwrap ├── testdata │ └── src │ │ ├── c │ │ └── c.go │ │ ├── b │ │ └── b.go │ │ ├── a │ │ ├── a.go │ │ └── a.go.golden │ │ ├── e │ │ ├── e.go │ │ └── e.go.golden │ │ └── d │ │ └── d.go ├── errwrap_test.go └── errwrap.go ├── .gitignore ├── go.sum ├── main.go ├── .goreleaser.yml ├── .github └── workflows │ └── go.yml ├── LICENSE └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fatih/errwrap 2 | 3 | go 1.23.0 4 | 5 | require golang.org/x/tools v0.31.0 6 | 7 | require ( 8 | golang.org/x/mod v0.24.0 // indirect 9 | golang.org/x/sync v0.12.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /errwrap/testdata/src/c/c.go: -------------------------------------------------------------------------------- 1 | package c 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for %s with error: ", "foo", err) // want `Errorf call needs 1 arg but has 2 args` 11 | } 12 | -------------------------------------------------------------------------------- /errwrap/testdata/src/b/b.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for with error: ", err) // want `Errorf call has arguments but no formatting directives` 11 | } 12 | -------------------------------------------------------------------------------- /errwrap/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for %s with error: %s", "foo", err) // want `call could wrap the error with error-wrapping directive %w` 11 | } 12 | -------------------------------------------------------------------------------- /errwrap/testdata/src/a/a.go.golden: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for %s with error: %w", "foo", err) // want `call could wrap the error with error-wrapping directive %w` 11 | } 12 | -------------------------------------------------------------------------------- /errwrap/testdata/src/e/e.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for %s with error: %s", "foo", err.Error()) // want `call could wrap the error with error-wrapping directive %w` 11 | } 12 | -------------------------------------------------------------------------------- /errwrap/testdata/src/e/e.go.golden: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | return fmt.Errorf("failed for %s with error: %w", "foo", err) // want `call could wrap the error with error-wrapping directive %w` 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | internal/errwrap/testdata/pkg/mod 15 | 16 | -------------------------------------------------------------------------------- /errwrap/testdata/src/d/d.go: -------------------------------------------------------------------------------- 1 | package d 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | func foo() error { 9 | err := errors.New("bar!") 10 | err2 := errors.New("bar!") 11 | return fmt.Errorf("failed with errors %w, %w", err, err2) // want `Errorf call has more than one error-wrapping directive %w` 12 | } 13 | 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 4 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 5 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 6 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 7 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 8 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/fatih/errwrap/errwrap" 10 | "golang.org/x/tools/go/analysis/singlechecker" 11 | ) 12 | 13 | var ( 14 | version string 15 | date string 16 | ) 17 | 18 | func main() { 19 | // this is a small hack to implement the -V flag that is part of 20 | // go/analysis framework. It'll allow us to print the version with -V, but 21 | // the --help message will print the flags of the analyzer 22 | ff := flag.NewFlagSet("errwrap", flag.ContinueOnError) 23 | v := ff.Bool("V", false, "print version and exit") 24 | ff.Usage = func() {} 25 | ff.SetOutput(io.Discard) 26 | 27 | ff.Parse(os.Args[1:]) 28 | if *v { 29 | fmt.Printf("errwrap version %s (%s)\n", version, date) 30 | os.Exit(0) 31 | } 32 | singlechecker.Main(errwrap.Analyzer) 33 | } 34 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: errwrap 2 | release: 3 | prerelease: auto # don't publish release with -rc1,-pre, etc suffixes 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | ldflags: 12 | - -s -w -X main.version={{.Version}} -X main.date={{.Date}} 13 | binary: "errwrap" 14 | nfpms: 15 | - maintainer: Fatih Arslan 16 | description: Go tool to wrap and fix errors with the new %w verb directive 17 | homepage: https://github.com/fatih/errwrap 18 | license: BSD 3-Clause 19 | formats: 20 | - deb 21 | - rpm 22 | replacements: 23 | darwin: macOS 24 | archives: 25 | - replacements: 26 | darwin: macOS 27 | format_overrides: 28 | - goos: windows 29 | format: zip 30 | snapshot: 31 | name_template: "{{ .Tag }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^docs:' 37 | - '^test:' 38 | -------------------------------------------------------------------------------- /errwrap/errwrap_test.go: -------------------------------------------------------------------------------- 1 | package errwrap_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/errwrap/errwrap" 7 | "golang.org/x/tools/go/analysis/analysistest" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | testdata := analysistest.TestData() 12 | 13 | for _, tcase := range []struct { 14 | name string 15 | dir string 16 | }{ 17 | { 18 | name: "wrap the error with error-wrapping directive", 19 | dir: "a", 20 | }, 21 | { 22 | name: "has arguments but no formatting directives", 23 | dir: "b", 24 | }, 25 | { 26 | name: "has leftover arguments", 27 | dir: "c", 28 | }, 29 | { 30 | name: "too many formatting directives", 31 | dir: "d", 32 | }, 33 | { 34 | name: "wrap the error.String() with error-wrapping directive", 35 | dir: "e", 36 | }, 37 | } { 38 | t.Run(tcase.name, func(t *testing.T) { 39 | analysistest.RunWithSuggestedFixes(t, testdata, errwrap.Analyzer, tcase.dir) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | 12 | jobs: 13 | 14 | test-build: 15 | name: Test & Build 16 | runs-on: ubuntu-latest 17 | steps: 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: go.mod 26 | 27 | - name: Run go mod tidy 28 | run: | 29 | set -e 30 | go mod tidy 31 | output=$(git status -s) 32 | if [ -z "${output}" ]; then 33 | exit 0 34 | fi 35 | echo 'We wish to maintain a tidy state for go mod. Please run `go mod tidy` on your branch, commit and push again.' 36 | echo 'Running `go mod tidy` on this CI test yields with the following changes:' 37 | echo "$output" 38 | exit 1 39 | 40 | - name: Test 41 | run: | 42 | go test -race ./... 43 | 44 | - name: Staticcheck 45 | uses: dominikh/staticcheck-action@v1.3.1 46 | with: 47 | version: "2025.1.1" 48 | install-go: false 49 | 50 | - name: Build 51 | run: go build ./... 52 | 53 | - name: Run GoReleaser 54 | uses: goreleaser/goreleaser-action@v2 55 | # only release on tags 56 | if: success() && startsWith(github.ref, 'refs/tags/') 57 | with: 58 | version: latest 59 | args: release --rm-dist 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.ERRWRAP_ACTIONS_BOT_TOKEN }} 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Fatih Arslan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | This software includes some portions from Go. Go is used under the terms of the 32 | BSD like license. 33 | 34 | Copyright (c) 2012 The Go Authors. All rights reserved. 35 | 36 | Redistribution and use in source and binary forms, with or without 37 | modification, are permitted provided that the following conditions are 38 | met: 39 | 40 | * Redistributions of source code must retain the above copyright 41 | notice, this list of conditions and the following disclaimer. 42 | * Redistributions in binary form must reproduce the above 43 | copyright notice, this list of conditions and the following disclaimer 44 | in the documentation and/or other materials provided with the 45 | distribution. 46 | * Neither the name of Google Inc. nor the names of its 47 | contributors may be used to endorse or promote products derived from 48 | this software without specific prior written permission. 49 | 50 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 51 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 52 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 53 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 54 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 55 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 56 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 57 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 58 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 59 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 60 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 61 | 62 | The Go gopher was designed by Renee French. http://reneefrench.blogspot.com/ The design is licensed under the Creative Commons 3.0 Attributions license. Read this article for more details: https://blog.golang.org/gopher 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # errwrap [![](https://github.com/fatih/errwrap/workflows/build/badge.svg)](https://github.com/fatih/errwrap/actions) 2 | 3 | Wrap and fix Go errors with the new **`%w`** verb directive. This tool analyzes 4 | `fmt.Errorf()` calls and reports calls that contain a verb directive that is 5 | different than the new `%w` verb directive [introduced in Go v1.13](https://golang.org/doc/go1.13#error_wrapping). It's also capable of rewriting calls to use the new `%w` wrap verb directive. 6 | 7 | ![errwrap](https://user-images.githubusercontent.com/438920/69905498-26b34c80-1369-11ea-888d-608f32678971.gif) 8 | 9 | ## Install 10 | 11 | ```bash 12 | # minimum v1.16 is required 13 | go install github.com/fatih/errwrap@latest 14 | ``` 15 | 16 | or download one of the [pre-compiled binaries from the releases page](https://github.com/fatih/errwrap/releases/latest) and copy to the desired location. 17 | 18 | ## Usage 19 | 20 | By default, `errwrap` prints the output of the analyzer to stdout. You can pass 21 | a file, directory or a Go package: 22 | 23 | ```sh 24 | $ errwrap foo.go # pass a file 25 | $ errwrap ./... # recursively analyze all files 26 | $ errwrap github.com/fatih/gomodifytags # or pass a package 27 | ``` 28 | 29 | When called it displays the error with the line and column: 30 | 31 | ``` 32 | gomodifytags@v1.0.1/main.go:200:16: call could wrap the error with error-wrapping directive %w 33 | gomodifytags@v1.0.1/main.go:641:17: call could wrap the error with error-wrapping directive %w 34 | gomodifytags@v1.0.1/main.go:749:15: call could wrap the error with error-wrapping directive %w 35 | ``` 36 | 37 | `errwrap` is also able to rewrite your source code to replace any verb 38 | directive used for an `error` type with the `%w` verb directive. Assume we have 39 | the following source code: 40 | 41 | ``` 42 | $ cat demo.go 43 | package main 44 | 45 | import ( 46 | "errors" 47 | "fmt" 48 | ) 49 | 50 | func main() { 51 | _ = foo() 52 | } 53 | 54 | func foo() error { 55 | err := errors.New("bar!") 56 | return fmt.Errorf("foo failed: %s: %s bar ...", "foo", err) 57 | } 58 | ``` 59 | 60 | Calling `errwrap` with the `-fix` flag will rewrite the source code: 61 | 62 | ``` 63 | $ errwrap -fix main.go 64 | main.go:14:9: call could wrap the error with error-wrapping directive %w 65 | ``` 66 | 67 | ```diff 68 | diff --git a/main.go b/main.go 69 | index 41d1c42..6cb42b8 100644 70 | --- a/main.go 71 | +++ b/main.go 72 | @@ -11,5 +11,5 @@ func main() { 73 | 74 | func foo() error { 75 | err := errors.New("bar!") 76 | - return fmt.Errorf("failed for %s with error: %s", "foo", err) 77 | + return fmt.Errorf("failed for %s with error: %w", "foo", err) 78 | } 79 | ``` 80 | 81 | ## Whether to Wrap or not? 82 | 83 | Wrapping an error is not always the best approach. Wrapping exposes the 84 | underlying error and makes it part of your public API. This means clients who 85 | rely on them could see breaking changes if you change your underlying 86 | implementation or don't wrap anymore. 87 | 88 | The blog post **[Working with Errors in Go 89 | 1.13](https://blog.golang.org/go1.13-errors)** contains a section called 90 | `Whether to Wrap` that explains this in more detail 91 | 92 | 93 | ## Credits 94 | 95 | This tool is built on top of the excellent `go/analysis` package that makes it 96 | easy to write custom analyzers in Go. If you're interested in writing a tool, 97 | check out my **[Using go/analysis to write a custom 98 | linter](https://arslan.io/2019/06/13/using-go-analysis-to-write-a-custom-linter/)** 99 | blog post. 100 | 101 | Also part of the code that parses the verb directives is from the 102 | `go/analysis/passes/printf` analyzer. It's a simplified version and might 103 | contain discrepancies. 104 | -------------------------------------------------------------------------------- /errwrap/errwrap.go: -------------------------------------------------------------------------------- 1 | // Package errwrap defines an Analyzer that rewrites error statements to use 2 | // the new wrapping/unwrapping functionality 3 | package errwrap 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "go/ast" 9 | "go/constant" 10 | "go/printer" 11 | "go/token" 12 | "go/types" 13 | "strconv" 14 | "strings" 15 | "unicode/utf8" 16 | 17 | "golang.org/x/tools/go/analysis" 18 | "golang.org/x/tools/go/analysis/passes/inspect" 19 | "golang.org/x/tools/go/ast/inspector" 20 | "golang.org/x/tools/go/types/typeutil" 21 | ) 22 | 23 | // Analyzer of the linter 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "errwrap", 26 | Doc: "wrap errors in fmt.Errorf() calls with the %w verb directive", 27 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 28 | Run: run, 29 | RunDespiteErrors: true, 30 | } 31 | 32 | // Run is the runner for an analysis pass 33 | func run(pass *analysis.Pass) (interface{}, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.CallExpr)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | call := n.(*ast.CallExpr) 42 | 43 | fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func) 44 | if fn == nil { 45 | return 46 | } 47 | 48 | // for now only check these functions 49 | if fn.FullName() != "fmt.Errorf" { 50 | return 51 | } 52 | 53 | oldExpr := render(pass.Fset, call) 54 | 55 | format, idx := formatString(pass, call) 56 | if idx < 0 { 57 | // call has arguments but no formatting directives 58 | return 59 | } 60 | 61 | firstArg := idx + 1 // Arguments are immediately after format string. 62 | if !strings.Contains(format, "%") { 63 | if len(call.Args) > firstArg { 64 | pass.Reportf(call.Lparen, "%s call has arguments but no formatting directives", fn.Name()) 65 | } 66 | return 67 | } 68 | 69 | var hasError bool 70 | var errIndex int 71 | for i, arg := range call.Args { 72 | if t := pass.TypesInfo.TypeOf(arg); t != nil { 73 | switch t.String() { 74 | case "error": 75 | hasError = true 76 | errIndex = i 77 | case "string": 78 | if expr, ok := arg.(*ast.CallExpr); ok { 79 | if sel, ok := expr.Fun.(*ast.SelectorExpr); ok { 80 | if id, ok := sel.X.(*ast.Ident); ok { 81 | if pass.TypesInfo.TypeOf(id).String() == "error" { 82 | hasError = true 83 | errIndex = i 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | if !hasError { 93 | return 94 | } 95 | 96 | argNum := firstArg 97 | maxArgNum := firstArg 98 | anyIndex := false 99 | anyW := false 100 | newFormat := []byte(format) 101 | for i, w := 0, 0; i < len(format); i += w { 102 | w = 1 103 | if format[i] != '%' { 104 | continue 105 | } 106 | 107 | state := parsePrintfVerb(pass, call, fn.Name(), format[i:], firstArg, argNum) 108 | if state == nil { 109 | return 110 | } 111 | 112 | w = len(state.format) 113 | if state.hasIndex { 114 | anyIndex = true 115 | } 116 | 117 | if len(state.argNums) > 0 { 118 | // Continue with the next sequential argument. 119 | argNum = state.argNums[len(state.argNums)-1] + 1 120 | } 121 | 122 | for _, n := range state.argNums { 123 | if n >= maxArgNum { 124 | maxArgNum = n + 1 125 | } 126 | } 127 | 128 | if state.verb == 'w' { 129 | if anyW { 130 | pass.Reportf(call.Pos(), "%s call has more than one error-wrapping directive %%w", state.name) 131 | return 132 | } 133 | anyW = true 134 | continue 135 | } 136 | 137 | if state.argNum != errIndex { 138 | continue 139 | } 140 | 141 | newFormat[i+1] = 'w' 142 | 143 | newCall := &ast.CallExpr{ 144 | Fun: call.Fun, 145 | Args: make([]ast.Expr, len(call.Args)), 146 | Lparen: call.Lparen, 147 | Ellipsis: call.Ellipsis, 148 | Rparen: call.Rparen, 149 | } 150 | copy(newCall.Args, call.Args) 151 | 152 | if bl, ok := call.Args[0].(*ast.BasicLit); ok { 153 | // replace the expression, keep the arguments the same 154 | newCall.Args[0] = &ast.BasicLit{ 155 | Value: strconv.Quote(string(newFormat)), 156 | ValuePos: bl.ValuePos, 157 | Kind: bl.Kind, 158 | } 159 | } 160 | 161 | if pass.TypesInfo.TypeOf(call.Args[errIndex]).String() == "string" { 162 | arg := call.Args[errIndex] 163 | if expr, ok := arg.(*ast.CallExpr); ok { 164 | if sel, ok := expr.Fun.(*ast.SelectorExpr); ok { 165 | if id, ok := sel.X.(*ast.Ident); ok { 166 | // remove the .String call from the error 167 | newCall.Args[errIndex] = &ast.BasicLit{ 168 | Value: id.String(), 169 | ValuePos: sel.X.Pos(), 170 | Kind: token.STRING, 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | newExpr := render(pass.Fset, newCall) 178 | 179 | pass.Report(analysis.Diagnostic{ 180 | Pos: call.Pos(), 181 | Message: "call could wrap the error with error-wrapping directive %w", 182 | SuggestedFixes: []analysis.SuggestedFix{ 183 | { 184 | Message: fmt.Sprintf("should replace `%s` with `%s`", oldExpr, newExpr), 185 | TextEdits: []analysis.TextEdit{ 186 | { 187 | Pos: call.Pos(), 188 | End: call.End(), 189 | NewText: []byte(newExpr), 190 | }, 191 | }, 192 | }, 193 | }, 194 | }) 195 | } 196 | 197 | // Dotdotdot is hard. 198 | if call.Ellipsis.IsValid() && maxArgNum >= len(call.Args)-1 { 199 | return 200 | } 201 | // If any formats are indexed, extra arguments are ignored. 202 | if anyIndex { 203 | return 204 | } 205 | // There should be no leftover arguments. 206 | if maxArgNum != len(call.Args) { 207 | expect := maxArgNum - firstArg 208 | numArgs := len(call.Args) - firstArg 209 | pass.Reportf(call.Pos(), "%s call needs %v but has %v", fn.Name(), count(expect, "arg"), count(numArgs, "arg")) 210 | } 211 | 212 | // If any formats are indexed, extra arguments are ignored. 213 | if anyIndex { 214 | return 215 | } 216 | }) 217 | 218 | return nil, nil 219 | } 220 | 221 | // render returns the pretty-print of the given node 222 | func render(fset *token.FileSet, x interface{}) string { 223 | var buf bytes.Buffer 224 | if err := printer.Fprint(&buf, fset, x); err != nil { 225 | panic(err) 226 | } 227 | return buf.String() 228 | } 229 | 230 | // 231 | // NOTE(arslan): Copied from go/analysis/passes/printf/printf.go 232 | // 233 | 234 | // formatState holds the parsed representation of a printf directive such as "%3.*[4]d". 235 | // It is constructed by parsePrintfVerb. 236 | type formatState struct { 237 | verb rune // the format verb: 'd' for "%d" 238 | format string // the full format directive from % through verb, "%.3d". 239 | name string // Printf, Sprintf etc. 240 | flags []byte // the list of # + etc. 241 | argNums []int // the successive argument numbers that are consumed, adjusted to refer to actual arg in call 242 | firstArg int // Index of first argument after the format in the Printf call. 243 | 244 | // Used only during parse. 245 | pass *analysis.Pass 246 | call *ast.CallExpr 247 | argNum int // Which argument we're expecting to format now. 248 | hasIndex bool // Whether the argument is indexed. 249 | indexPending bool // Whether we have an indexed argument that has not resolved. 250 | nbytes int // number of bytes of the format string consumed. 251 | } 252 | 253 | // formatString returns the format string argument and its index within 254 | // the given printf-like call expression. 255 | // 256 | // The last parameter before variadic arguments is assumed to be 257 | // a format string. 258 | // 259 | // The first string literal or string constant is assumed to be a format string 260 | // if the call's signature cannot be determined. 261 | // 262 | // If it cannot find any format string parameter, it returns ("", -1). 263 | func formatString(pass *analysis.Pass, call *ast.CallExpr) (format string, idx int) { 264 | typ := pass.TypesInfo.Types[call.Fun].Type 265 | if typ != nil { 266 | if sig, ok := typ.(*types.Signature); ok { 267 | if !sig.Variadic() { 268 | // Skip checking non-variadic functions. 269 | return "", -1 270 | } 271 | idx := sig.Params().Len() - 2 272 | if idx < 0 { 273 | // Skip checking variadic functions without 274 | // fixed arguments. 275 | return "", -1 276 | } 277 | s, ok := stringConstantArg(pass, call, idx) 278 | if !ok { 279 | // The last argument before variadic args isn't a string. 280 | return "", -1 281 | } 282 | return s, idx 283 | } 284 | } 285 | 286 | // Cannot determine call's signature. Fall back to scanning for the first 287 | // string constant in the call. 288 | for idx := range call.Args { 289 | if s, ok := stringConstantArg(pass, call, idx); ok { 290 | return s, idx 291 | } 292 | if pass.TypesInfo.Types[call.Args[idx]].Type == types.Typ[types.String] { 293 | // Skip checking a call with a non-constant format 294 | // string argument, since its contents are unavailable 295 | // for validation. 296 | return "", -1 297 | } 298 | } 299 | return "", -1 300 | } 301 | 302 | // stringConstantArg returns call's string constant argument at the index idx. 303 | // 304 | // ("", false) is returned if call's argument at the index idx isn't a string 305 | // constant. 306 | func stringConstantArg(pass *analysis.Pass, call *ast.CallExpr, idx int) (string, bool) { 307 | if idx >= len(call.Args) { 308 | return "", false 309 | } 310 | arg := call.Args[idx] 311 | lit := pass.TypesInfo.Types[arg].Value 312 | if lit != nil && lit.Kind() == constant.String { 313 | return constant.StringVal(lit), true 314 | } 315 | return "", false 316 | } 317 | 318 | // parsePrintfVerb looks the formatting directive that begins the format string 319 | // and returns a formatState that encodes what the directive wants, without looking 320 | // at the actual arguments present in the call. The result is nil if there is an error. 321 | func parsePrintfVerb(pass *analysis.Pass, call *ast.CallExpr, name, format string, firstArg, argNum int) *formatState { 322 | state := &formatState{ 323 | format: format, 324 | name: name, 325 | flags: make([]byte, 0, 5), 326 | argNum: argNum, 327 | argNums: make([]int, 0, 1), 328 | nbytes: 1, // There's guaranteed to be a percent sign. 329 | firstArg: firstArg, 330 | pass: pass, 331 | call: call, 332 | } 333 | // There may be flags. 334 | state.parseFlags() 335 | // There may be an index. 336 | if !state.parseIndex() { 337 | return nil 338 | } 339 | // There may be a width. 340 | if !state.parseNum() { 341 | return nil 342 | } 343 | // There may be a precision. 344 | if !state.parsePrecision() { 345 | return nil 346 | } 347 | // Now a verb, possibly prefixed by an index (which we may already have). 348 | if !state.indexPending && !state.parseIndex() { 349 | return nil 350 | } 351 | 352 | if state.nbytes == len(state.format) { 353 | pass.Reportf(call.Pos(), "%s format %s is missing verb at end of string", name, state.format) 354 | return nil 355 | } 356 | verb, w := utf8.DecodeRuneInString(state.format[state.nbytes:]) 357 | state.verb = verb 358 | state.nbytes += w 359 | if verb != '%' { 360 | state.argNums = append(state.argNums, state.argNum) 361 | } 362 | state.format = state.format[:state.nbytes] 363 | return state 364 | } 365 | 366 | // parseFlags accepts any printf flags. 367 | func (s *formatState) parseFlags() { 368 | for s.nbytes < len(s.format) { 369 | switch c := s.format[s.nbytes]; c { 370 | case '#', '0', '+', '-', ' ': 371 | s.flags = append(s.flags, c) 372 | s.nbytes++ 373 | default: 374 | return 375 | } 376 | } 377 | } 378 | 379 | // scanNum advances through a decimal number if present. 380 | func (s *formatState) scanNum() { 381 | for ; s.nbytes < len(s.format); s.nbytes++ { 382 | c := s.format[s.nbytes] 383 | if c < '0' || '9' < c { 384 | return 385 | } 386 | } 387 | } 388 | 389 | // parseIndex scans an index expression. It returns false if there is a syntax error. 390 | func (s *formatState) parseIndex() bool { 391 | if s.nbytes == len(s.format) || s.format[s.nbytes] != '[' { 392 | return true 393 | } 394 | // Argument index present. 395 | s.nbytes++ // skip '[' 396 | start := s.nbytes 397 | s.scanNum() 398 | ok := true 399 | if s.nbytes == len(s.format) || s.nbytes == start || s.format[s.nbytes] != ']' { 400 | ok = false 401 | s.nbytes = strings.Index(s.format, "]") 402 | if s.nbytes < 0 { 403 | s.pass.Reportf(s.call.Pos(), "%s format %s is missing closing ]", s.name, s.format) 404 | return false 405 | } 406 | } 407 | arg32, err := strconv.ParseInt(s.format[start:s.nbytes], 10, 32) 408 | if err != nil || !ok || arg32 <= 0 || arg32 > int64(len(s.call.Args)-s.firstArg) { 409 | s.pass.Reportf(s.call.Pos(), "%s format has invalid argument index [%s]", s.name, s.format[start:s.nbytes]) 410 | return false 411 | } 412 | s.nbytes++ // skip ']' 413 | arg := int(arg32) 414 | arg += s.firstArg - 1 // We want to zero-index the actual arguments. 415 | s.argNum = arg 416 | s.hasIndex = true 417 | s.indexPending = true 418 | return true 419 | } 420 | 421 | // parseNum scans a width or precision (or *). It returns false if there's a bad index expression. 422 | func (s *formatState) parseNum() bool { 423 | if s.nbytes < len(s.format) && s.format[s.nbytes] == '*' { 424 | if s.indexPending { // Absorb it. 425 | s.indexPending = false 426 | } 427 | s.nbytes++ 428 | s.argNums = append(s.argNums, s.argNum) 429 | s.argNum++ 430 | } else { 431 | s.scanNum() 432 | } 433 | return true 434 | } 435 | 436 | // parsePrecision scans for a precision. It returns false if there's a bad index expression. 437 | func (s *formatState) parsePrecision() bool { 438 | // If there's a period, there may be a precision. 439 | if s.nbytes < len(s.format) && s.format[s.nbytes] == '.' { 440 | s.flags = append(s.flags, '.') // Treat precision as a flag. 441 | s.nbytes++ 442 | if !s.parseIndex() { 443 | return false 444 | } 445 | if !s.parseNum() { 446 | return false 447 | } 448 | } 449 | return true 450 | } 451 | 452 | // count(n, what) returns "1 what" or "N whats" 453 | // (assuming the plural of what is whats). 454 | func count(n int, what string) string { 455 | if n == 1 { 456 | return "1 " + what 457 | } 458 | return fmt.Sprintf("%d %ss", n, what) 459 | } 460 | --------------------------------------------------------------------------------