woodpecker/vendor/github.com/gostaticanalysis/nilerr/nilerr.go

292 lines
5.9 KiB
Go
Raw Normal View History

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
}