276 lines
7.3 KiB
Go
276 lines
7.3 KiB
Go
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package nilness inspects the control-flow graph of an SSA function
|
|
// and reports errors such as nil pointer dereferences and degenerate
|
|
// nil pointer comparisons.
|
|
|
|
// This is a copy of https://github.com/golang/tools/blob/master/go/analysis/passes/nilness/nilness.go
|
|
// from the commit f0bfdbff1f9c986484a9f02fc198b1efcfe76ebe.
|
|
// Can't use the original one because of https://github.com/golang/go/issues/29612
|
|
package nilness
|
|
|
|
import (
|
|
"fmt"
|
|
"go/token"
|
|
"go/types"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/passes/buildssa"
|
|
"golang.org/x/tools/go/ssa"
|
|
)
|
|
|
|
const Doc = `check for redundant or impossible nil comparisons
|
|
|
|
The nilness checker inspects the control-flow graph of each function in
|
|
a package and reports nil pointer dereferences and degenerate nil
|
|
pointers. A degenerate comparison is of the form x==nil or x!=nil where x
|
|
is statically known to be nil or non-nil. These are often a mistake,
|
|
especially in control flow related to errors.
|
|
|
|
This check reports conditions such as:
|
|
|
|
if f == nil { // impossible condition (f is a function)
|
|
}
|
|
|
|
and:
|
|
|
|
p := &v
|
|
...
|
|
if p != nil { // tautological condition
|
|
}
|
|
|
|
and:
|
|
|
|
if p == nil {
|
|
print(*p) // nil dereference
|
|
}
|
|
`
|
|
|
|
var Analyzer = &analysis.Analyzer{
|
|
Name: "nilness",
|
|
Doc: Doc,
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{buildssa.Analyzer},
|
|
}
|
|
|
|
func run(pass *analysis.Pass) (interface{}, error) {
|
|
ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
|
|
for _, fn := range ssainput.SrcFuncs {
|
|
runFunc(pass, fn)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func runFunc(pass *analysis.Pass, fn *ssa.Function) {
|
|
reportf := func(category string, pos token.Pos, format string, args ...interface{}) {
|
|
pass.Report(analysis.Diagnostic{
|
|
Pos: pos,
|
|
Category: category,
|
|
Message: fmt.Sprintf(format, args...),
|
|
})
|
|
}
|
|
|
|
// notNil reports an error if v is provably nil.
|
|
notNil := func(stack []fact, instr ssa.Instruction, v ssa.Value, descr string) {
|
|
if nilnessOf(stack, v) == isnil {
|
|
reportf("nilderef", instr.Pos(), "nil dereference in "+descr)
|
|
}
|
|
}
|
|
|
|
// visit visits reachable blocks of the CFG in dominance order,
|
|
// maintaining a stack of dominating nilness facts.
|
|
//
|
|
// By traversing the dom tree, we can pop facts off the stack as
|
|
// soon as we've visited a subtree. Had we traversed the CFG,
|
|
// we would need to retain the set of facts for each block.
|
|
seen := make([]bool, len(fn.Blocks)) // seen[i] means visit should ignore block i
|
|
var visit func(b *ssa.BasicBlock, stack []fact)
|
|
visit = func(b *ssa.BasicBlock, stack []fact) {
|
|
if seen[b.Index] {
|
|
return
|
|
}
|
|
seen[b.Index] = true
|
|
|
|
// Report nil dereferences.
|
|
for _, instr := range b.Instrs {
|
|
switch instr := instr.(type) {
|
|
case ssa.CallInstruction:
|
|
notNil(stack, instr, instr.Common().Value,
|
|
instr.Common().Description())
|
|
case *ssa.FieldAddr:
|
|
notNil(stack, instr, instr.X, "field selection")
|
|
case *ssa.IndexAddr:
|
|
notNil(stack, instr, instr.X, "index operation")
|
|
case *ssa.MapUpdate:
|
|
notNil(stack, instr, instr.Map, "map update")
|
|
case *ssa.Slice:
|
|
// A nilcheck occurs in ptr[:] iff ptr is a pointer to an array.
|
|
if _, ok := instr.X.Type().Underlying().(*types.Pointer); ok {
|
|
notNil(stack, instr, instr.X, "slice operation")
|
|
}
|
|
case *ssa.Store:
|
|
notNil(stack, instr, instr.Addr, "store")
|
|
case *ssa.TypeAssert:
|
|
notNil(stack, instr, instr.X, "type assertion")
|
|
case *ssa.UnOp:
|
|
if instr.Op == token.MUL { // *X
|
|
notNil(stack, instr, instr.X, "load")
|
|
}
|
|
}
|
|
}
|
|
|
|
// For nil comparison blocks, report an error if the condition
|
|
// is degenerate, and push a nilness fact on the stack when
|
|
// visiting its true and false successor blocks.
|
|
if binop, tsucc, fsucc := eq(b); binop != nil {
|
|
xnil := nilnessOf(stack, binop.X)
|
|
ynil := nilnessOf(stack, binop.Y)
|
|
|
|
if ynil != unknown && xnil != unknown && (xnil == isnil || ynil == isnil) {
|
|
// Degenerate condition:
|
|
// the nilness of both operands is known,
|
|
// and at least one of them is nil.
|
|
var adj string
|
|
if (xnil == ynil) == (binop.Op == token.EQL) {
|
|
adj = "tautological"
|
|
} else {
|
|
adj = "impossible"
|
|
}
|
|
reportf("cond", binop.Pos(), "%s condition: %s %s %s", adj, xnil, binop.Op, ynil)
|
|
|
|
// If tsucc's or fsucc's sole incoming edge is impossible,
|
|
// it is unreachable. Prune traversal of it and
|
|
// all the blocks it dominates.
|
|
// (We could be more precise with full dataflow
|
|
// analysis of control-flow joins.)
|
|
var skip *ssa.BasicBlock
|
|
if xnil == ynil {
|
|
skip = fsucc
|
|
} else {
|
|
skip = tsucc
|
|
}
|
|
for _, d := range b.Dominees() {
|
|
if d == skip && len(d.Preds) == 1 {
|
|
continue
|
|
}
|
|
visit(d, stack)
|
|
}
|
|
return
|
|
}
|
|
|
|
// "if x == nil" or "if nil == y" condition; x, y are unknown.
|
|
if xnil == isnil || ynil == isnil {
|
|
var f fact
|
|
if xnil == isnil {
|
|
// x is nil, y is unknown:
|
|
// t successor learns y is nil.
|
|
f = fact{binop.Y, isnil}
|
|
} else {
|
|
// x is nil, y is unknown:
|
|
// t successor learns x is nil.
|
|
f = fact{binop.X, isnil}
|
|
}
|
|
|
|
for _, d := range b.Dominees() {
|
|
// Successor blocks learn a fact
|
|
// only at non-critical edges.
|
|
// (We could do be more precise with full dataflow
|
|
// analysis of control-flow joins.)
|
|
s := stack
|
|
if len(d.Preds) == 1 {
|
|
if d == tsucc {
|
|
s = append(s, f)
|
|
} else if d == fsucc {
|
|
s = append(s, f.negate())
|
|
}
|
|
}
|
|
visit(d, s)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, d := range b.Dominees() {
|
|
visit(d, stack)
|
|
}
|
|
}
|
|
|
|
// Visit the entry block. No need to visit fn.Recover.
|
|
if fn.Blocks != nil {
|
|
visit(fn.Blocks[0], make([]fact, 0, 20)) // 20 is plenty
|
|
}
|
|
}
|
|
|
|
// A fact records that a block is dominated
|
|
// by the condition v == nil or v != nil.
|
|
type fact struct {
|
|
value ssa.Value
|
|
nilness nilness
|
|
}
|
|
|
|
func (f fact) negate() fact { return fact{f.value, -f.nilness} }
|
|
|
|
type nilness int
|
|
|
|
const (
|
|
isnonnil = -1
|
|
unknown nilness = 0
|
|
isnil = 1
|
|
)
|
|
|
|
var nilnessStrings = []string{"non-nil", "unknown", "nil"}
|
|
|
|
func (n nilness) String() string { return nilnessStrings[n+1] }
|
|
|
|
// nilnessOf reports whether v is definitely nil, definitely not nil,
|
|
// or unknown given the dominating stack of facts.
|
|
func nilnessOf(stack []fact, v ssa.Value) nilness {
|
|
// Is value intrinsically nil or non-nil?
|
|
switch v := v.(type) {
|
|
case *ssa.Alloc,
|
|
*ssa.FieldAddr,
|
|
*ssa.FreeVar,
|
|
*ssa.Function,
|
|
*ssa.Global,
|
|
*ssa.IndexAddr,
|
|
*ssa.MakeChan,
|
|
*ssa.MakeClosure,
|
|
*ssa.MakeInterface,
|
|
*ssa.MakeMap,
|
|
*ssa.MakeSlice:
|
|
return isnonnil
|
|
case *ssa.Const:
|
|
if v.IsNil() {
|
|
return isnil
|
|
} else {
|
|
return isnonnil
|
|
}
|
|
}
|
|
|
|
// Search dominating control-flow facts.
|
|
for _, f := range stack {
|
|
if f.value == v {
|
|
return f.nilness
|
|
}
|
|
}
|
|
return unknown
|
|
}
|
|
|
|
// If b ends with an equality comparison, eq returns the operation and
|
|
// its true (equal) and false (not equal) successors.
|
|
func eq(b *ssa.BasicBlock) (op *ssa.BinOp, tsucc, fsucc *ssa.BasicBlock) {
|
|
if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok {
|
|
if binop, ok := If.Cond.(*ssa.BinOp); ok {
|
|
switch binop.Op {
|
|
case token.EQL:
|
|
return binop, b.Succs[0], b.Succs[1]
|
|
case token.NEQ:
|
|
return binop, b.Succs[1], b.Succs[0]
|
|
}
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|