// 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 import ( _ "embed" "fmt" "go/token" "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ssa" "golang.org/x/tools/internal/typeparams" ) //go:embed doc.go var doc string var Analyzer = &analysis.Analyzer{ Name: "nilness", Doc: analysisutil.MustExtractDoc(doc, "nilness"), URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilness", 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{}) { // We ignore nil-checking ssa.Instructions // that don't correspond to syntax. if pos.IsValid() { 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(), 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: // A nil receiver may be okay for type params. cc := instr.Common() if !(cc.IsInvoke() && typeparams.IsTypeParam(cc.Value.Type())) { notNil(stack, instr, cc.Value, "nil dereference in "+cc.Description()) } case *ssa.FieldAddr: notNil(stack, instr, instr.X, "nil dereference in field selection") case *ssa.IndexAddr: switch typeparams.CoreType(instr.X.Type()).(type) { case *types.Pointer: // *array notNil(stack, instr, instr.X, "nil dereference in array index operation") case *types.Slice: // This is not necessarily a runtime error, because // it is usually dominated by a bounds check. if isRangeIndex(instr) { notNil(stack, instr, instr.X, "range of nil slice") } else { notNil(stack, instr, instr.X, "index of nil slice") } } case *ssa.MapUpdate: notNil(stack, instr, instr.Map, "nil dereference in map update") case *ssa.Range: // (Not a runtime error, but a likely mistake.) notNil(stack, instr, instr.X, "range over nil map") case *ssa.Slice: // A nilcheck occurs in ptr[:] iff ptr is a pointer to an array. if is[*types.Pointer](instr.X.Type().Underlying()) { notNil(stack, instr, instr.X, "nil dereference in slice operation") } case *ssa.Store: notNil(stack, instr, instr.Addr, "nil dereference in store") case *ssa.TypeAssert: if !instr.CommaOk { notNil(stack, instr, instr.X, "nil dereference in type assertion") } case *ssa.UnOp: switch instr.Op { case token.MUL: // *X notNil(stack, instr, instr.X, "nil dereference in load") case token.ARROW: // <-ch // (Not a runtime error, but a likely mistake.) notNil(stack, instr, instr.X, "receive from nil channel") } case *ssa.Send: // (Not a runtime error, but a likely mistake.) notNil(stack, instr, instr.Chan, "send to nil channel") } } // 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") } case *ssa.SliceToArrayPointer: nn := nilnessOf(stack, instr.X) if nn == isnil && slice2ArrayPtrLen(instr) > 0 { reportf("conversionpanic", instr.Pos(), "nil slice being cast to an array of len > 0 will always panic") } } } // 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 } } // In code of the form: // // if ptr, ok := x.(*T); ok { ... } else { fsucc } // // the fsucc block learns that ptr == nil, // since that's its zero value. if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { // Handle "if ok" and "if !ok" variants. cond, fsucc := If.Cond, b.Succs[1] if unop, ok := cond.(*ssa.UnOp); ok && unop.Op == token.NOT { cond, fsucc = unop.X, b.Succs[0] } // Match pattern: // t0 = typeassert (pointerlike) // t1 = extract t0 #0 // ptr // t2 = extract t0 #1 // ok // if t2 goto tsucc, fsucc if extract1, ok := cond.(*ssa.Extract); ok && extract1.Index == 1 { if assert, ok := extract1.Tuple.(*ssa.TypeAssert); ok && isNillable(assert.AssertedType) { for _, pinstr := range *assert.Referrers() { if extract0, ok := pinstr.(*ssa.Extract); ok && extract0.Index == 0 && extract0.Tuple == extract1.Tuple { for _, d := range b.Dominees() { if len(d.Preds) == 1 && d == fsucc { visit(d, append(stack, fact{extract0, isnil})) } } } } } } } 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 and Slice 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 } case *ssa.Slice: if underlying := nilnessOf(stack, v.X); underlying != unknown { return underlying } case *ssa.SliceToArrayPointer: nn := nilnessOf(stack, v.X) if slice2ArrayPtrLen(v) > 0 { if nn == isnil { // We know that *(*[1]byte)(nil) is going to panic because of the // conversion. So return unknown to the caller, prevent useless // nil deference reporting due to * operator. return unknown } // Otherwise, the conversion will yield a non-nil pointer to array. // Note that the instruction can still panic if array length greater // than slice length. If the value is used by another instruction, // that instruction can assume the panic did not happen when that // instruction is reached. return isnonnil } // In case array length is zero, the conversion result depends on nilness of the slice. if nn != unknown { return nn } } // 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 // nil or zero value of a pointer-like type } else { return unknown // non-pointer } } // Search dominating control-flow facts. for _, f := range stack { if f.value == v { return f.nilness } } return unknown } func slice2ArrayPtrLen(v *ssa.SliceToArrayPointer) int64 { return v.Type().(*types.Pointer).Elem().Underlying().(*types.Array).Len() } // 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 happened 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 } func is[T any](x any) bool { _, ok := x.(T) return ok } func isNillable(t types.Type) bool { switch t := typeparams.CoreType(t).(type) { case *types.Pointer, *types.Map, *types.Signature, *types.Chan, *types.Interface, *types.Slice: return true case *types.Basic: return t == types.Typ[types.UnsafePointer] } return false } // isRangeIndex reports whether the instruction is a slice indexing // operation slice[i] within a "for range slice" loop. The operation // could be explicit, such as slice[i] within (or even after) the // loop, or it could be implicit, such as "for i, v := range slice {}". // (These cannot be reliably distinguished.) func isRangeIndex(instr *ssa.IndexAddr) bool { // Here we reverse-engineer the go/ssa lowering of range-over-slice: // // n = len(x) // jump loop // loop: "rangeindex.loop" // phi = φ(-1, incr) #rangeindex // incr = phi + 1 // cond = incr < n // if cond goto body else done // body: "rangeindex.body" // instr = &x[incr] // ... // done: if incr, ok := instr.Index.(*ssa.BinOp); ok && incr.Op == token.ADD { if b := incr.Block(); b.Comment == "rangeindex.loop" { if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { if cond := If.Cond.(*ssa.BinOp); cond.X == incr && cond.Op == token.LSS { if call, ok := cond.Y.(*ssa.Call); ok { common := call.Common() if blt, ok := common.Value.(*ssa.Builtin); ok && blt.Name() == "len" { return common.Args[0] == instr.X } } } } } } return false }