diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2021-02-22 20:37:25 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2021-02-22 21:02:12 +0100 |
| commit | fcc6d71be2c3ce7d9305c04fc2e87af554571bac (patch) | |
| tree | b01dbb3d1e2988e28ea158d2d543d603ec0b9569 /vendor/github.com/tomarrell/wrapcheck | |
| parent | 8f23c528ad5a943b9ffec5dcaf332fd0f614006e (diff) | |
go.mod: update golangci-lint to v1.37
Diffstat (limited to 'vendor/github.com/tomarrell/wrapcheck')
| -rw-r--r-- | vendor/github.com/tomarrell/wrapcheck/LICENSE | 21 | ||||
| -rw-r--r-- | vendor/github.com/tomarrell/wrapcheck/wrapcheck/wrapcheck.go | 226 |
2 files changed, 247 insertions, 0 deletions
diff --git a/vendor/github.com/tomarrell/wrapcheck/LICENSE b/vendor/github.com/tomarrell/wrapcheck/LICENSE new file mode 100644 index 000000000..b5d9d30d3 --- /dev/null +++ b/vendor/github.com/tomarrell/wrapcheck/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Tom Arrell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/tomarrell/wrapcheck/wrapcheck/wrapcheck.go b/vendor/github.com/tomarrell/wrapcheck/wrapcheck/wrapcheck.go new file mode 100644 index 000000000..e249a7ed2 --- /dev/null +++ b/vendor/github.com/tomarrell/wrapcheck/wrapcheck/wrapcheck.go @@ -0,0 +1,226 @@ +package wrapcheck + +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" +) + +var ignoredIDs = []string{ + "func fmt.Errorf(format string, a ...interface{}) error", + "func errors.New(text string) error", + "func errors.Unwrap(err error) error", +} + +var Analyzer = &analysis.Analyzer{ + Name: "wrapcheck", + Doc: "Checks that errors returned from external packages are wrapped", + Run: run, +} + +func run(pass *analysis.Pass) (interface{}, error) { + for _, file := range pass.Files { + ast.Inspect(file, func(n ast.Node) bool { + if _, ok := n.(*ast.AssignStmt); ok { + return true + } + + ret, ok := n.(*ast.ReturnStmt) + if !ok { + return true + } + + if len(ret.Results) < 1 { + return true + } + + // Iterate over the values to be returned looking for errors + for _, expr := range ret.Results { + if !isError(pass.TypesInfo.TypeOf(expr)) { + continue + } + + ident, ok := expr.(*ast.Ident) + if !ok { + return true + } + + var ( + call *ast.CallExpr + ) + + // Attempt to find the most recent short assign + if shortAss := prevErrAssign(pass, file, ident); shortAss != nil { + call, ok = shortAss.Rhs[0].(*ast.CallExpr) + if !ok { + return true + } + } else if isUnresolved(file, ident) { + // TODO Check if the identifier is unresolved, and try to resolve it in + // another file. + return true + } else { + // Check for ValueSpec nodes in order to locate a possible var + // declaration. + if ident.Obj == nil { + return true + } + + vSpec, ok := ident.Obj.Decl.(*ast.ValueSpec) + if !ok { + // We couldn't find a short or var assign for this error return. + // This is an error. Where did this identifier come from? Possibly a + // function param. + // + // TODO decide how to handle this case, whether to follow function + // param back, or assert wrapping at call site. + + return true + } + + if len(vSpec.Values) < 1 { + return true + } + + call, ok = vSpec.Values[0].(*ast.CallExpr) + if !ok { + return true + } + } + + // Make sure there is a call identified as producing the error being + // returned, otherwise just bail + if call == nil { + return true + } + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + // Check if the underlying type of the "x" in x.y.z is an interface, as + // errors returned from interface types should be wrapped. + if isInterface(pass, sel, ident) { + pass.Reportf(ident.NamePos, "error returned from interface method should be wrapped") + return true + } + + // Check whether the function being called comes from another package, + // as functions called across package boundaries which returns errors + // should be wrapped + if isFromOtherPkg(pass, sel, ident) { + pass.Reportf(ident.NamePos, "error returned from external package is unwrapped") + return true + } + } + + return true + }) + } + + return nil, nil +} + +// isInterface returns whether the function call is one defined on an interface. +func isInterface(pass *analysis.Pass, sel *ast.SelectorExpr, ident *ast.Ident) bool { + _, ok := pass.TypesInfo.TypeOf(sel.X).Underlying().(*types.Interface) + + return ok +} + +func isFromOtherPkg(pass *analysis.Pass, sel *ast.SelectorExpr, ident *ast.Ident) bool { + // The package of the function that we are calling which returns the error + fn := pass.TypesInfo.ObjectOf(sel.Sel) + + // If it's not a package name, then we should check the selector to make sure + // that it's an identifier from the same package + if pass.Pkg.Path() == fn.Pkg().Path() { + return false + } else if contains(ignoredIDs, fn.String()) { + return false + } + + return true +} + +// prevErrAssign traverses the AST of a file looking for the most recent +// assignment to an error declaration which is specified by the returnIdent +// identifier. +// +// This only returns short form assignments and reassignments, e.g. `:=` and +// `=`. This does not include `var` statements. This function will return nil if +// the only declaration is a `var` (aka ValueSpec) declaration. +func prevErrAssign(pass *analysis.Pass, file *ast.File, returnIdent *ast.Ident) *ast.AssignStmt { + // A slice containing all the assignments which contain an identifer + // referring to the source declaration of the error. This is to catch + // cases where err is defined once, and then reassigned multiple times + // within the same block. In these cases, we should check the method of + // the most recent call. + var assigns []*ast.AssignStmt + + // Find all assignments which have the same declaration + ast.Inspect(file, func(n ast.Node) bool { + if ass, ok := n.(*ast.AssignStmt); ok { + for _, expr := range ass.Lhs { + if !isError(pass.TypesInfo.TypeOf(expr)) { + continue + } + if assIdent, ok := expr.(*ast.Ident); ok { + if assIdent.Obj == nil || returnIdent.Obj == nil { + // If we can't find the Obj for one of the identifiers, just skip + // it. + return true + } else if assIdent.Obj.Decl == returnIdent.Obj.Decl { + assigns = append(assigns, ass) + } + } + } + } + + return true + }) + + // Iterate through the assignments, comparing the token positions to + // find the assignment that directly precedes the return position + var mostRecentAssign *ast.AssignStmt + + for _, ass := range assigns { + if ass.Pos() > returnIdent.Pos() { + break + } + mostRecentAssign = ass + } + + return mostRecentAssign +} + +func contains(slice []string, el string) bool { + for _, s := range slice { + if s == el { + return true + } + } + + return false +} + +// isError returns whether or not the provided type interface is an error +func isError(typ types.Type) bool { + if typ == nil { + return false + } + + return typ.String() == "error" +} + +func isUnresolved(file *ast.File, ident *ast.Ident) bool { + for _, unresolvedIdent := range file.Unresolved { + if unresolvedIdent.Pos() == ident.Pos() { + return true + } + } + + return false +} |
