logginghandler/vendor/github.com/polyfloyd/go-errorlint/errorlint/lint.go
Marvin Preuss d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
build: uses go modules for tool handling
2022-01-14 13:51:56 +01:00

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"
}