250 lines
6.0 KiB
Go
250 lines
6.0 KiB
Go
package errorlint
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/token"
|
|
"go/types"
|
|
"regexp"
|
|
)
|
|
|
|
type Lint struct {
|
|
Message string
|
|
Pos token.Pos
|
|
}
|
|
|
|
type ByPosition []Lint
|
|
|
|
func (l ByPosition) Len() int { return len(l) }
|
|
func (l ByPosition) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
|
|
|
func (l ByPosition) Less(i, j int) bool {
|
|
return l[i].Pos < l[j].Pos
|
|
}
|
|
|
|
func LintFmtErrorfCalls(fset *token.FileSet, info types.Info) []Lint {
|
|
lints := []Lint{}
|
|
for expr, t := range info.Types {
|
|
// Search for error expressions that are the result of fmt.Errorf
|
|
// invocations.
|
|
if t.Type.String() != "error" {
|
|
continue
|
|
}
|
|
call, ok := isFmtErrorfCallExpr(info, expr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Find all % fields in the format string.
|
|
formatVerbs, ok := printfFormatStringVerbs(info, call)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// For any arguments that are errors, check whether the wrapping verb
|
|
// is used. Only one %w verb may be used in a single format string at a
|
|
// time, so we stop after finding a correct %w.
|
|
var lintArg ast.Expr
|
|
args := call.Args[1:]
|
|
for i := 0; i < len(args) && i < len(formatVerbs); i++ {
|
|
if info.Types[args[i]].Type.String() != "error" && !isErrorStringCall(info, args[i]) {
|
|
continue
|
|
}
|
|
|
|
if formatVerbs[i] == "%w" {
|
|
lintArg = nil
|
|
break
|
|
}
|
|
|
|
if lintArg == nil {
|
|
lintArg = args[i]
|
|
}
|
|
}
|
|
if lintArg != nil {
|
|
lints = append(lints, Lint{
|
|
Message: "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors",
|
|
Pos: lintArg.Pos(),
|
|
})
|
|
}
|
|
}
|
|
return lints
|
|
}
|
|
|
|
// isErrorStringCall tests whether the expression is a string expression that
|
|
// is the result of an `(error).Error()` method call.
|
|
func isErrorStringCall(info types.Info, expr ast.Expr) bool {
|
|
if info.Types[expr].Type.String() == "string" {
|
|
if call, ok := expr.(*ast.CallExpr); ok {
|
|
if callSel, ok := call.Fun.(*ast.SelectorExpr); ok {
|
|
fun := info.Uses[callSel.Sel].(*types.Func)
|
|
return fun.Type().String() == "func() string" && fun.Name() == "Error"
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func printfFormatStringVerbs(info types.Info, call *ast.CallExpr) ([]string, bool) {
|
|
if len(call.Args) <= 1 {
|
|
return nil, false
|
|
}
|
|
strLit, ok := call.Args[0].(*ast.BasicLit)
|
|
if !ok {
|
|
// Ignore format strings that are not literals.
|
|
return nil, false
|
|
}
|
|
formatString := constant.StringVal(info.Types[strLit].Value)
|
|
|
|
// Naive format string argument verb. This does not take modifiers such as
|
|
// padding into account...
|
|
re := regexp.MustCompile(`%[^%]`)
|
|
return re.FindAllString(formatString, -1), true
|
|
}
|
|
|
|
func isFmtErrorfCallExpr(info types.Info, expr ast.Expr) (*ast.CallExpr, bool) {
|
|
call, ok := expr.(*ast.CallExpr)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
fn, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
// TODO: Support fmt.Errorf variable aliases?
|
|
return nil, false
|
|
}
|
|
obj := info.Uses[fn.Sel]
|
|
|
|
pkg := obj.Pkg()
|
|
if pkg != nil && pkg.Name() == "fmt" && obj.Name() == "Errorf" {
|
|
return call, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func LintErrorComparisons(fset *token.FileSet, info types.Info) []Lint {
|
|
lints := []Lint{}
|
|
|
|
for expr := range info.Types {
|
|
// Find == and != operations.
|
|
binExpr, ok := expr.(*ast.BinaryExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if binExpr.Op != token.EQL && binExpr.Op != token.NEQ {
|
|
continue
|
|
}
|
|
// Comparing errors with nil is okay.
|
|
if isNilComparison(binExpr) {
|
|
continue
|
|
}
|
|
// Find comparisons of which one side is a of type error.
|
|
if !isErrorComparison(info, binExpr) {
|
|
continue
|
|
}
|
|
|
|
if isAllowedErrorComparison(info, binExpr) {
|
|
continue
|
|
}
|
|
|
|
lints = append(lints, Lint{
|
|
Message: fmt.Sprintf("comparing with %s will fail on wrapped errors. Use errors.Is to check for a specific error", binExpr.Op),
|
|
Pos: binExpr.Pos(),
|
|
})
|
|
}
|
|
|
|
for scope := range info.Scopes {
|
|
// Find value switch blocks.
|
|
switchStmt, ok := scope.(*ast.SwitchStmt)
|
|
if !ok {
|
|
continue
|
|
}
|
|
// Check whether the switch operates on an error type.
|
|
if switchStmt.Tag == nil {
|
|
continue
|
|
}
|
|
tagType := info.Types[switchStmt.Tag]
|
|
if tagType.Type.String() != "error" {
|
|
continue
|
|
}
|
|
|
|
lints = append(lints, Lint{
|
|
Message: "switch on an error will fail on wrapped errors. Use errors.Is to check for specific errors",
|
|
Pos: switchStmt.Pos(),
|
|
})
|
|
}
|
|
|
|
return lints
|
|
}
|
|
|
|
func isNilComparison(binExpr *ast.BinaryExpr) bool {
|
|
if ident, ok := binExpr.X.(*ast.Ident); ok && ident.Name == "nil" {
|
|
return true
|
|
}
|
|
if ident, ok := binExpr.Y.(*ast.Ident); ok && ident.Name == "nil" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isErrorComparison(info types.Info, binExpr *ast.BinaryExpr) bool {
|
|
tx := info.Types[binExpr.X]
|
|
ty := info.Types[binExpr.Y]
|
|
return tx.Type.String() == "error" || ty.Type.String() == "error"
|
|
}
|
|
|
|
func LintErrorTypeAssertions(fset *token.FileSet, info types.Info) []Lint {
|
|
lints := []Lint{}
|
|
|
|
for expr := range info.Types {
|
|
// Find type assertions.
|
|
typeAssert, ok := expr.(*ast.TypeAssertExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Find type assertions that operate on values of type error.
|
|
if !isErrorTypeAssertion(info, typeAssert) {
|
|
continue
|
|
}
|
|
|
|
lints = append(lints, Lint{
|
|
Message: "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors",
|
|
Pos: typeAssert.Pos(),
|
|
})
|
|
}
|
|
|
|
for scope := range info.Scopes {
|
|
// Find type switches.
|
|
typeSwitch, ok := scope.(*ast.TypeSwitchStmt)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Find the type assertion in the type switch.
|
|
var typeAssert *ast.TypeAssertExpr
|
|
switch t := typeSwitch.Assign.(type) {
|
|
case *ast.ExprStmt:
|
|
typeAssert = t.X.(*ast.TypeAssertExpr)
|
|
case *ast.AssignStmt:
|
|
typeAssert = t.Rhs[0].(*ast.TypeAssertExpr)
|
|
}
|
|
|
|
// Check whether the type switch is on a value of type error.
|
|
if !isErrorTypeAssertion(info, typeAssert) {
|
|
continue
|
|
}
|
|
|
|
lints = append(lints, Lint{
|
|
Message: "type switch on error will fail on wrapped errors. Use errors.As to check for specific errors",
|
|
Pos: typeAssert.Pos(),
|
|
})
|
|
}
|
|
|
|
return lints
|
|
}
|
|
|
|
func isErrorTypeAssertion(info types.Info, typeAssert *ast.TypeAssertExpr) bool {
|
|
t := info.Types[typeAssert.X]
|
|
return t.Type.String() == "error"
|
|
}
|