Marvin Preuss
d095180eb4
All checks were successful
continuous-integration/drone/push Build is passing
292 lines
5.9 KiB
Go
292 lines
5.9 KiB
Go
package nilerr
|
|
|
|
import (
|
|
"fmt"
|
|
"go/token"
|
|
"go/types"
|
|
|
|
"github.com/gostaticanalysis/comment"
|
|
"github.com/gostaticanalysis/comment/passes/commentmap"
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/buildssa"
|
|
"golang.org/x/tools/go/ssa"
|
|
)
|
|
|
|
var Analyzer = &analysis.Analyzer{
|
|
Name: "nilerr",
|
|
Doc: Doc,
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{
|
|
buildssa.Analyzer,
|
|
commentmap.Analyzer,
|
|
},
|
|
}
|
|
|
|
const Doc = "nilerr checks returning nil when err is not nil"
|
|
|
|
func run(pass *analysis.Pass) (interface{}, error) {
|
|
funcs := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA).SrcFuncs
|
|
cmaps := pass.ResultOf[commentmap.Analyzer].(comment.Maps)
|
|
|
|
reportFail := func(v ssa.Value, ret *ssa.Return, format string) {
|
|
pos := ret.Pos()
|
|
line := getNodeLineNumber(pass, ret)
|
|
errLines := getValueLineNumbers(pass, v)
|
|
if !cmaps.IgnoreLine(pass.Fset, line, "nilerr") {
|
|
var errLineText string
|
|
if len(errLines) == 1 {
|
|
errLineText = fmt.Sprintf("line %d", errLines[0])
|
|
} else {
|
|
errLineText = fmt.Sprintf("lines %v", errLines)
|
|
}
|
|
pass.Reportf(pos, format, errLineText)
|
|
}
|
|
}
|
|
|
|
for i := range funcs {
|
|
for _, b := range funcs[i].Blocks {
|
|
if v := binOpErrNil(b, token.NEQ); v != nil {
|
|
if ret := isReturnNil(b.Succs[0]); ret != nil {
|
|
if !usesErrorValue(b.Succs[0], v) {
|
|
reportFail(v, ret, "error is not nil (%s) but it returns nil")
|
|
}
|
|
}
|
|
} else if v := binOpErrNil(b, token.EQL); v != nil {
|
|
if len(b.Succs[0].Preds) == 1 { // if there are multiple conditions, this may be false positive
|
|
if ret := isReturnError(b.Succs[0], v); ret != nil {
|
|
reportFail(v, ret, "error is nil (%s) but it returns error")
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func getValueLineNumbers(pass *analysis.Pass, v ssa.Value) []int {
|
|
if phi, ok := v.(*ssa.Phi); ok {
|
|
result := make([]int, 0, len(phi.Edges))
|
|
for _, edge := range phi.Edges {
|
|
result = append(result, getValueLineNumbers(pass, edge)...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
value := v
|
|
if extract, ok := value.(*ssa.Extract); ok {
|
|
value = extract.Tuple
|
|
}
|
|
|
|
pos := value.Pos()
|
|
return []int{pass.Fset.File(pos).Line(pos)}
|
|
}
|
|
|
|
func getNodeLineNumber(pass *analysis.Pass, node ssa.Node) int {
|
|
pos := node.Pos()
|
|
return pass.Fset.File(pos).Line(pos)
|
|
}
|
|
|
|
var errType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
|
|
|
|
func binOpErrNil(b *ssa.BasicBlock, op token.Token) ssa.Value {
|
|
if len(b.Instrs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ifinst, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
binop, ok := ifinst.Cond.(*ssa.BinOp)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if binop.Op != op {
|
|
return nil
|
|
}
|
|
|
|
if !types.Implements(binop.X.Type(), errType) {
|
|
return nil
|
|
}
|
|
|
|
if !types.Implements(binop.Y.Type(), errType) {
|
|
return nil
|
|
}
|
|
|
|
xIsConst, yIsConst := isConst(binop.X), isConst(binop.Y)
|
|
switch {
|
|
case !xIsConst && yIsConst: // err != nil or err == nil
|
|
return binop.X
|
|
case xIsConst && !yIsConst: // nil != err or nil == err
|
|
return binop.Y
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isConst(v ssa.Value) bool {
|
|
_, ok := v.(*ssa.Const)
|
|
return ok
|
|
}
|
|
|
|
func isReturnNil(b *ssa.BasicBlock) *ssa.Return {
|
|
if len(b.Instrs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
errorReturnValues := 0
|
|
for _, res := range ret.Results {
|
|
if !types.Implements(res.Type(), errType) {
|
|
continue
|
|
}
|
|
|
|
errorReturnValues++
|
|
v, ok := res.(*ssa.Const)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if !v.IsNil() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if errorReturnValues == 0 {
|
|
return nil
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func isReturnError(b *ssa.BasicBlock, errVal ssa.Value) *ssa.Return {
|
|
if len(b.Instrs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ret, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
for _, v := range ret.Results {
|
|
if v == errVal {
|
|
return ret
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func usesErrorValue(b *ssa.BasicBlock, errVal ssa.Value) bool {
|
|
for _, instr := range b.Instrs {
|
|
if callInstr, ok := instr.(*ssa.Call); ok {
|
|
for _, arg := range callInstr.Call.Args {
|
|
if isUsedInValue(arg, errVal) {
|
|
return true
|
|
}
|
|
|
|
sliceArg, ok := arg.(*ssa.Slice)
|
|
if ok {
|
|
if isUsedInSlice(sliceArg, errVal) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type ReferrersHolder interface {
|
|
Referrers() *[]ssa.Instruction
|
|
}
|
|
|
|
var _ ReferrersHolder = (ssa.Node)(nil)
|
|
var _ ReferrersHolder = (ssa.Value)(nil)
|
|
|
|
func isUsedInSlice(sliceArg *ssa.Slice, errVal ssa.Value) bool {
|
|
var valueBuf [10]*ssa.Value
|
|
operands := sliceArg.Operands(valueBuf[:0])
|
|
|
|
var valuesToInspect []ssa.Value
|
|
addValueForInspection := func(value ssa.Value) {
|
|
if value != nil {
|
|
valuesToInspect = append(valuesToInspect, value)
|
|
}
|
|
}
|
|
|
|
var nodesToInspect []ssa.Node
|
|
visitedNodes := map[ssa.Node]bool{}
|
|
addNodeForInspection := func(node ssa.Node) {
|
|
if !visitedNodes[node] {
|
|
visitedNodes[node] = true
|
|
nodesToInspect = append(nodesToInspect, node)
|
|
}
|
|
}
|
|
addReferrersForInspection := func(h ReferrersHolder) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
|
|
referrers := h.Referrers()
|
|
if referrers == nil {
|
|
return
|
|
}
|
|
|
|
for _, r := range *referrers {
|
|
if node, ok := r.(ssa.Node); ok {
|
|
addNodeForInspection(node)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, operand := range operands {
|
|
addReferrersForInspection(*operand)
|
|
addValueForInspection(*operand)
|
|
}
|
|
|
|
for i := 0; i < len(nodesToInspect); i++ {
|
|
switch node := nodesToInspect[i].(type) {
|
|
case *ssa.IndexAddr:
|
|
addReferrersForInspection(node)
|
|
case *ssa.Store:
|
|
addValueForInspection(node.Val)
|
|
}
|
|
}
|
|
|
|
for _, value := range valuesToInspect {
|
|
if isUsedInValue(value, errVal) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isUsedInValue(value, lookedFor ssa.Value) bool {
|
|
if value == lookedFor {
|
|
return true
|
|
}
|
|
|
|
switch value := value.(type) {
|
|
case *ssa.ChangeInterface:
|
|
return isUsedInValue(value.X, lookedFor)
|
|
case *ssa.MakeInterface:
|
|
return isUsedInValue(value.X, lookedFor)
|
|
case *ssa.Call:
|
|
if value.Call.IsInvoke() {
|
|
return isUsedInValue(value.Call.Value, lookedFor)
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|