// 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. 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, degenerate nil pointers, and panics with nil values. 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. Panics with nil values are checked because they are not detectable by if r := recover(); r != nil { 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 } and: if p == nil { panic(p) } ` 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: if !instr.CommaOk { notNil(stack, instr, instr.X, "type assertion") } case *ssa.UnOp: if instr.Op == token.MUL { // *X notNil(stack, instr, instr.X, "load") } } } // Look for panics with nil value for _, instr := range b.Instrs { switch instr := instr.(type) { case *ssa.Panic: if nilnessOf(stack, instr.X) == isnil { reportf("nilpanic", instr.Pos(), "panic with nil value") } } } // 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 newFacts facts if xnil == isnil { // x is nil, y is unknown: // t successor learns y is nil. newFacts = expandFacts(fact{binop.Y, isnil}) } else { // x is nil, y is unknown: // t successor learns x is nil. newFacts = expandFacts(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, newFacts...) } else if d == fsucc { s = append(s, newFacts.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 { switch v := v.(type) { // unwrap ChangeInterface values recursively, to detect if underlying // values have any facts recorded or are otherwise known with regard to nilness. // // This work must be in addition to expanding facts about // ChangeInterfaces during inference/fact gathering because this covers // cases where the nilness of a value is intrinsic, rather than based // on inferred facts, such as a zero value interface variable. That // said, this work alone would only inform us when facts are about // underlying values, rather than outer values, when the analysis is // transitive in both directions. case *ssa.ChangeInterface: if underlying := nilnessOf(stack, v.X); underlying != unknown { return underlying } } // 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 } // expandFacts takes a single fact and returns the set of facts that can be // known about it or any of its related values. Some operations, like // ChangeInterface, have transitive nilness, such that if you know the // underlying value is nil, you also know the value itself is nil, and vice // versa. This operation allows callers to match on any of the related values // in analyses, rather than just the one form of the value that happend to // appear in a comparison. // // This work must be in addition to unwrapping values within nilnessOf because // while this work helps give facts about transitively known values based on // inferred facts, the recursive check within nilnessOf covers cases where // nilness facts are intrinsic to the underlying value, such as a zero value // interface variables. // // ChangeInterface is the only expansion currently supported, but others, like // Slice, could be added. At this time, this tool does not check slice // operations in a way this expansion could help. See // https://play.golang.org/p/mGqXEp7w4fR for an example. func expandFacts(f fact) []fact { ff := []fact{f} Loop: for { switch v := f.value.(type) { case *ssa.ChangeInterface: f = fact{v.X, f.nilness} ff = append(ff, f) default: break Loop } } return ff } type facts []fact func (ff facts) negate() facts { nn := make([]fact, len(ff)) for i, f := range ff { nn[i] = f.negate() } return nn }