Files
raven/vendor/honnef.co/go/tools/analysis/facts/nilness/nilness.go
2026-08-21 10:49:49 +00:00

831 lines
22 KiB
Go

package nilness
import (
"fmt"
"go/constant"
"go/token"
"go/types"
"reflect"
"slices"
"strings"
"honnef.co/go/tools/analysis/dfa"
"honnef.co/go/tools/analysis/dfa/dense"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
)
// TODO(dh): The analysis is currently entirely forward, which means that for
//
// x := s[:0]
// y := s[:1]
// z := s[:0]
//
// x will have MaybeNil at every program point and s will have MaybeNil before
// execution of y, even though executing y without panicing tells us that s has
// been non-nil for all 3 instructions.
type nilnessFact struct {
Rets []ValueNilness
}
func (*nilnessFact) AFact() {}
func (fact *nilnessFact) String() string {
return fmt.Sprintf("nilness: %v", fact.Rets)
}
type ValueNilness struct {
// Undefined for non-interface values.
// For interface values, whether the stored value may be nil.
// Even when Outer == MaybeNil, Inner may still offer precise information
// for the cases when Outer is dynamically not nil. For example, {NeverNil,
// MaybeNil} states that the interface value might be nil, but if it isn't,
// it will definitely contain a non-nil value.
Inner Nilness
// For non-interface values, whether the value may be nil.
// For interface values, whether the interface value may be nil.
Outer Nilness
}
type Result struct {
m map[*types.Func][]ValueNilness
}
var Analysis = &analysis.Analyzer{
Name: "nilness",
Doc: "Annotates return values with their nilness",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
FactTypes: []analysis.Fact{(*nilnessFact)(nil)},
ResultType: reflect.TypeFor[*Result](),
}
// Nilness returns nilness information for return value ret of fn.
func (r *Result) Nilness(fn *types.Func, ret int) ValueNilness {
typ := fn.Type().(*types.Signature).Results().At(ret).Type()
if !typeutil.IsPointerLike(typ) {
return ValueNilness{Outer: NeverNil}
}
if len(r.m[fn]) == 0 {
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
}
return normalize(r.m[fn][ret], typ)
}
func normalize(v ValueNilness, typ types.Type) ValueNilness {
if v.Inner == 0 || !types.IsInterface(typ) {
v.Inner = MaybeNil
}
if v.Outer == 0 {
v.Outer = MaybeNil
}
return v
}
func run(pass *analysis.Pass) (any, error) {
seen := map[*ir.Function]struct{}{}
out := &Result{
m: map[*types.Func][]ValueNilness{},
}
// TODO(dh): instead of recursion and giving up on mutual recursion, we
// should compute the DFA over the call graph, at least until we have
// proper function summaries.
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
impl(pass, fn, seen)
}
for _, fact := range pass.AllObjectFacts() {
out.m[fact.Object.(*types.Func)] = fact.Fact.(*nilnessFact).Rets
}
return out, nil
}
type Nilness uint8
const (
// The value is never nil.
NeverNil Nilness = iota + 1
// The value is always nil.
AlwaysNil
// The value might be nil, but only because of the value of a global
// variable.
MaybeNilGlobal
// The value might be nil.
MaybeNil
)
func (n Nilness) String() string {
switch n {
case 0:
return "NoNilness"
case NeverNil:
return "NeverNil"
case AlwaysNil:
return "AlwaysNil"
case MaybeNilGlobal:
return "MaybeNilGlobal"
case MaybeNil:
return "MaybeNil"
default:
return "InvalidNilness"
}
}
type state struct {
cloned bool
m []ValueNilness
n numbering
}
func (s *state) get(v ir.Value) ValueNilness {
if !typeutil.IsPointerLike(v.Type()) {
// All non-pointer-like types are always {_ NeverNil}.
return ValueNilness{Outer: NeverNil}
}
num := s.n.number(v)
if num < len(s.m) {
return s.m[num]
}
switch v.(type) {
case *ir.Parameter:
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
case *ir.Builtin:
return ValueNilness{Outer: NeverNil}
case *ir.FreeVar:
return ValueNilness{Inner: MaybeNil, Outer: MaybeNil}
case *ir.Function:
return ValueNilness{Outer: NeverNil}
case *ir.Global:
// Globals are addresses, not the values stored in them. The addresses
// cannot be nil.
return ValueNilness{Outer: NeverNil}
}
return lattice{}.Ident()
}
func (s *state) set(key ir.Value, value ValueNilness) {
if !typeutil.IsPointerLike(key.Type()) {
// No point in recording state for non-pointer-like types. They're
// always {_ NeverNil}.
return
}
if value == (lattice{}.Ident()) {
// No point in storing the default value.
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num] == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
}
s.m[num] = value
}
func (s *state) setInner(key ir.Value, value Nilness) {
if !typeutil.IsPointerLike(key.Type()) {
return
}
if value == (lattice{}.Ident().Inner) {
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num].Inner == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
dflt := s.get(key)
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
s.m[num] = dflt
}
v := s.m[num]
v.Inner = value
s.m[num] = v
}
func (s *state) setOuter(key ir.Value, value Nilness) {
if !typeutil.IsPointerLike(key.Type()) {
return
}
if value == (lattice{}).Ident().Outer {
return
}
num := s.n.number(key)
if !s.cloned {
if num < len(s.m) && s.m[num].Outer == value {
// Don't clone if the value already matches.
return
}
s.cloned = true
s.m = slices.Clone(s.m)
}
if num >= len(s.m) {
dflt := s.get(key)
s.m = append(s.m, make([]ValueNilness, num-len(s.m)+1)...)
s.m[num] = dflt
}
v := s.m[num]
v.Outer = value
s.m[num] = v
}
func defaultNilnessForSignature(pass *analysis.Pass, typ *types.Signature) []ValueNilness {
n := typ.Results().Len()
if n == 0 {
return nil
}
out := make([]ValueNilness, n)
for i := range n {
out[i] = defaultNilness(pass, typ.Results().At(i).Type())
}
return out
}
func defaultNilness(pass *analysis.Pass, typ types.Type) ValueNilness {
if typeutil.IsPointerLike(typ) {
// IsPointerLike handles type parameters with type sets, too.
return ValueNilness{MaybeNil, MaybeNil}
} else {
return ValueNilness{NeverNil, NeverNil}
}
}
func impl(pass *analysis.Pass, fn *ir.Function, seenFns map[*ir.Function]struct{}) []ValueNilness {
goto start
bailout:
return defaultNilnessForSignature(pass, fn.Signature)
start:
if fn.Signature.Results().Len() == 0 {
return nil
}
if fn.Object() == nil {
// TODO(dh): support closures
goto bailout
}
if fact := new(nilnessFact); pass.ImportObjectFact(fn.Object(), fact) {
return fact.Rets
}
if fn.Pkg != pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg {
goto bailout
}
if fn.Blocks == nil {
goto bailout
}
if _, ok := seenFns[fn]; ok {
// break recursion
goto bailout
}
seenFns[fn] = struct{}{}
anyPointers := false
for ret := range fn.Signature.Results().Variables() {
if typeutil.IsPointerLike(ret.Type()) {
anyPointers = true
break
}
}
if !anyPointers {
goto bailout
}
n := numbering{}
processBlock := func(from, to *ir.BasicBlock, s state) state {
handleReturnValue := func(v ir.Value, call *ir.Call, idx int) {
typ := call.Common().Signature().Results().At(idx).Type()
if !typeutil.IsPointerLike(typ) {
s.setOuter(v, NeverNil)
return
}
if callee, ok := call.Call.Value.(*ir.Builtin); ok {
switch callee.Name() {
case "append":
// TODO(dh): if we knew that the varargs had non-zero
// length, we'd know that the resulting slice is non-nil.
switch an := s.get(call.Call.Args[0]).Outer; an {
case MaybeNil, MaybeNilGlobal, NeverNil:
s.setOuter(v, an)
case AlwaysNil:
s.setOuter(v, MaybeNil)
}
case "UnsafeSlice":
// If len is negative, or if ptr is nil and len is not
// zero, unsafe.Slice panics. This implies that a non-nil
// pointer cannot become nil, and vice versa.
s.set(v, s.get(call.Call.Args[0]))
case "UnsafeStringData":
// TODO(dh): if we had string length information we could
// return better information.
s.setOuter(v, MaybeNil)
case "UnsafeSliceData":
// When the slice is non-nil but has zero capacity, the
// returned pointer is still non-nil, so we don't have to
// worry about that.
s.set(v, s.get(call.Call.Args[0]))
case "UnsafeAdd":
// TODO(dh): a positive addend can never result in a nil pointer.
// Pointer arithmetic can turn nil pointers into non-nil
// ones and vice versa.
s.setOuter(v, MaybeNil)
case "ssa:deferstack":
s.setOuter(v, NeverNil)
case "ssa:wrapnilchk":
s.setOuter(v, NeverNil)
case "recover":
s.setOuter(v, MaybeNil)
default:
panic(fmt.Sprintf("internal error: unhandled builtin %s", callee.Name()))
}
return
}
callee := call.Common().StaticCallee()
if callee == nil {
// We don't know which function is being called.
s.set(v, ValueNilness{MaybeNil, MaybeNil})
return
}
calleeNilness := impl(pass, callee, seenFns)
if len(calleeNilness) > idx {
s.set(v, normalize(calleeNilness[idx], typ))
} else {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
}
for _, instr := range from.Instrs {
// It is tempting to return early when instr is an ir.Value that
// doesn't have pointer type. However, instructions like ir.Load
// tell us something about the value being operated on.
switch v := instr.(type) {
case *ir.Convert:
s.set(v, s.get(v.X))
case *ir.SliceToArrayPointer:
// Go does not currently allow (*T)(s) where T is a type
// parameter with a type set consisting of array types, but it
// does allow (T)(s) where T is a type parameter with a type
// set consisting of pointers to array types.
allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
ptr := term.Type().Underlying().(*types.Pointer).Elem()
return typeutil.All(ptr, func(innerTerm *types.Term) bool {
return innerTerm.Type().Underlying().(*types.Array).Len() != 0
})
})
if allNonZero {
// converting a slice to an array pointer of length > 0
// panics if the slice is nil
s.setOuter(v, NeverNil)
s.setOuter(v.X, NeverNil)
} else {
s.set(v, s.get(v.X))
}
case *ir.SliceToArray:
// Pretty much the same logic as SliceToArrayPointer, minus the
// pointer.
allNonZero := typeutil.All(v.Type(), func(term *types.Term) bool {
return term.Type().Underlying().(*types.Array).Len() != 0
})
if allNonZero {
// converting a slice to an array of length > 0
// panics if the slice is nil
s.setOuter(v.X, NeverNil)
}
case *ir.Slice:
if typeutil.All(v.X.Type(), typeutil.IsType[*types.Array]) {
// Slicing arrays never results in a nil slice.
s.setOuter(v, NeverNil)
continue
}
// checkBound returns true if one of the bounds (low, high,
// capacity) has non-zero value.
checkBound := func(v ir.Value) bool {
if v == nil {
return false
}
// TODO(dh): this is where integration with constant
// propagation and value range analysis would be useful.
if k, ok := v.(*ir.Const); ok {
kv, ok := constant.Int64Val(k.Value)
return !ok || kv != 0
}
return false
}
if checkBound(v.Low) || checkBound(v.High) || checkBound(v.Max) {
// One of the indices is non-zero, which means slicing can
// only succeed if the slicee is not nil.
s.setOuter(v, NeverNil)
s.setOuter(v.X, NeverNil)
} else {
// The new slice is as nilly as the slicee.
s.set(v, s.get(v.X))
}
case *ir.If:
cond := v.Cond
binop, ok := cond.(*ir.BinOp)
if !ok {
continue
}
isNil := func(v ir.Value) bool {
k, ok := v.(*ir.Const)
if !ok {
return false
}
return k.Value == nil
}
var target ir.Value
if isNil(binop.X) {
target = binop.Y
} else if isNil(binop.Y) {
target = binop.X
} else {
continue
}
op := binop.Op
if to != from.Succs[0] {
// we're in the false branch, negate op
switch op {
case token.EQL:
op = token.NEQ
case token.NEQ:
op = token.EQL
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
}
switch op {
case token.EQL:
s.set(target, ValueNilness{AlwaysNil, AlwaysNil})
case token.NEQ:
s.setOuter(target, NeverNil)
default:
panic(fmt.Sprintf("internal error: unhandled token %v", op))
}
// TODO(dh): also handle comparison of two non-nil values. The
// true branch of neverNil == nilly makes the nilly value neverNil.
case *ir.ChangeType:
s.set(v, s.get(v.X))
case *ir.MultiConvert:
s.set(v, s.get(v.X))
case *ir.Load:
if _, ok := v.X.(*ir.Global); ok {
s.setOuter(v, MaybeNilGlobal)
} else {
s.setOuter(v, MaybeNil)
}
s.setOuter(v.X, NeverNil)
case *ir.FieldAddr:
s.setOuter(v.X, NeverNil)
s.setOuter(v, NeverNil)
case *ir.IndexAddr:
s.setOuter(v.X, NeverNil)
s.setOuter(v, NeverNil)
case *ir.Alloc, *ir.MakeMap, *ir.MakeSlice, *ir.MakeClosure, *ir.MakeChan:
s.setOuter(v.(ir.Value), NeverNil)
case *ir.MapUpdate:
s.setOuter(v.Map, NeverNil)
case *ir.Store:
s.setOuter(v.Addr, NeverNil)
case ir.CallInstruction:
// go/defer/calling a nil function fatals/panics
if !v.Common().IsInvoke() {
s.setOuter(v.Common().Value, NeverNil)
}
_, ok := v.(*ir.Call)
if !ok {
// Defer and Go don't produce values
continue
}
if v.Common().Signature().Results().Len() != 1 {
// If the called function doesn't return any values then we
// don't care about it. If it has more than one return
// value, they'll be handled by Extract.
continue
}
handleReturnValue(v.(ir.Value), v.(*ir.Call), 0)
case *ir.Send:
s.setOuter(v.Chan, NeverNil)
case *ir.Recv:
s.setOuter(v.Chan, NeverNil)
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.MakeInterface:
s.set(v, ValueNilness{
Inner: s.get(v.X).Outer,
Outer: NeverNil,
})
case *ir.ChangeInterface:
s.set(v, s.get(v.X))
case *ir.TypeAssert:
if !v.CommaOk {
// The interface value cannot have been nil, or the type
// assertion would have panicked.
s.setOuter(v.X, NeverNil)
if types.IsInterface(v.Type()) && !typeparams.IsTypeParam(v.Type()) {
// Type asserting to another interface doesn't succeed
// if the assertee was nil. It also results in a new
// interface value.
s.setOuter(v, NeverNil)
s.setInner(v, s.get(v.X).Inner)
} else {
// We've extracted the interface value's inner value.
s.setOuter(v, s.get(v.X).Inner)
}
} else {
// In a comma-ok type assertion, the return type is a
// tuple. There'll be Extract instructions getting the
// individual values, to which we'll attach the nilness
// info.
}
case *ir.TypeSwitch:
// Handled in Extract
case *ir.MapLookup:
if s.get(v.X).Outer == AlwaysNil {
s.set(v, ValueNilness{AlwaysNil, AlwaysNil})
} else {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
case *ir.Field:
s.set(v.X, ValueNilness{NeverNil, NeverNil})
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.Index:
s.set(v.X, ValueNilness{NeverNil, NeverNil})
s.set(v, ValueNilness{MaybeNil, MaybeNil})
case *ir.Extract:
switch tuple := v.Tuple.(type) {
case *ir.TypeAssert:
// When we get here, the type assertion used the comma-ok
// form, and we don't yet know anything about the result of
// the type assertion.
if v.Index == 0 {
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
// TODO(dh): We should set v's nilness in the true and
// false branches of checks on the ok value. However, ok can be
// used in arbitrary ways, and we're also not set up to handle
// relational facts (ok being true or false affects the value
// of the other Extract).
case *ir.Call:
handleReturnValue(v, tuple, v.Index)
case *ir.TypeSwitch:
if v.Index == 0 {
// Index 0 is an integer and not interesting.
continue
}
idx := v.Index - 1
if idx >= len(tuple.Conds) {
// Default branch
// If there is an untyped nil case, then being in the
// default branch tells us that the interface value
// isn't nil.
hasNil := slices.ContainsFunc(tuple.Conds, func(typ types.Type) bool {
if typ, ok := typ.(*types.Basic); ok && typ.Kind() == types.UntypedNil {
return true
}
return false
})
if hasNil {
s.setOuter(tuple.Tag, NeverNil)
} else {
s.setOuter(tuple.Tag, MaybeNil)
}
s.setOuter(v, s.get(tuple.Tag).Inner)
} else {
// There is no Extract for the 'untyped nil' case,
// which means that executing any Extract from a type
// switch implies that the switched-over value wasn't a
// nil interface value.
s.setOuter(tuple.Tag, NeverNil)
typ := tuple.Conds[idx]
if types.IsInterface(typ) && !typeparams.IsTypeParam(typ) {
// Succesfully type asserting to an interface type
// always produces a non-nil interface value.
s.setInner(v, s.get(tuple.Tag).Inner)
s.setOuter(v, NeverNil)
} else {
s.setOuter(v, s.get(tuple.Tag).Inner)
}
}
default:
s.set(v, ValueNilness{MaybeNil, MaybeNil})
}
case *ir.Select:
if v.Blocking && len(v.States) == 1 {
// If the select doesn't have a default branch and only has
// one state, that state's channel cannot have been nil if
// we finished execution the select.
s.setOuter(v.States[0].Chan, NeverNil)
}
case *ir.Jump, *ir.BlankStore, *ir.Phi,
*ir.Panic, *ir.Return, *ir.RunDefers, *ir.Unreachable, *ir.ConstantSwitch,
*ir.UnOp, *ir.BinOp, *ir.CompositeValue, *ir.Range, *ir.Next:
default:
posn := pass.Fset.PositionFor(v.Pos(), false)
panic(fmt.Sprintf("internal error: unhandled type %T at %s", v, posn))
}
}
return s
}
processPhis := func(b *ir.BasicBlock, i int, s state) state {
for _, instr := range b.Instrs {
if instr, ok := instr.(*ir.Phi); ok {
s.set(instr, s.get(instr.Edges[i]))
} else {
break
}
}
return s
}
// Populate default state for non-instruction values we encounter. We
// cannot defer this logic to state.get because control flow merges use
// simple merges of lattice values and won't know about value-specific
// defaults.
entrys := state{cloned: true, n: n}
for _, param := range fn.Params {
if typeutil.IsPointerLike(param.Type()) {
entrys.set(param, ValueNilness{Inner: MaybeNil, Outer: MaybeNil})
} else {
// We never track nilness for value types, so they don't have to be
// present in the entry state, either.
}
}
if strings.HasPrefix(fn.Synthetic, "bound method wrapper") {
// This is a bound method and the bound receiver might be nil
for _, fvar := range fn.FreeVars {
entrys.set(fvar, ValueNilness{Outer: MaybeNil})
}
} else {
// This is a closure, and closed over variables are allocs, which
// cannot be nil.
for _, fvar := range fn.FreeVars {
entrys.set(fvar, ValueNilness{Outer: NeverNil})
}
}
var ops []*ir.Value
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
ops = instr.Operands(ops[:0])
for _, pop := range ops {
if op, ok := (*pop).(*ir.Const); ok && typeutil.IsPointerLike(op.Type()) {
// The only constant pointer-like is nil.
entrys.set(op, ValueNilness{Inner: AlwaysNil, Outer: AlwaysNil})
}
}
}
}
res := dense.Forward[dfa.DenseMapLattice[ValueNilness, lattice]](
fn,
map[int][]ValueNilness{0: entrys.m},
func(fromID, toID int, in []ValueNilness) []ValueNilness {
from := fn.Blocks[fromID]
to := fn.Blocks[toID]
s := state{n: n, m: in}
s = processBlock(from, to, s)
i := slices.Index(to.Preds, from)
s = processPhis(to, i, s)
return s.m
},
)
retNilness := make([]ValueNilness, fn.Signature.Results().Len())
for b := range fn.Returns() {
ret := b.Control().(*ir.Return)
s := state{n: n, m: res.In(b.Index)}
s = processBlock(b, nil, s)
for i, res := range ret.Results {
retNilness[i] = lattice{}.Merge(retNilness[i], s.get(res))
}
}
interesting := false
for i := range retNilness {
typ := fn.Signature.Results().At(i).Type()
if !typeutil.IsPointerLike(typ) {
retNilness[i] = ValueNilness{NeverNil, NeverNil}
continue
}
retNilness[i] = normalize(retNilness[i], typ)
if retNilness[i] != (ValueNilness{MaybeNil, MaybeNil}) {
interesting = true
}
}
if interesting {
pass.ExportObjectFact(fn.Object(), &nilnessFact{retNilness})
}
return retNilness
}
type lattice struct{}
var _ dfa.Semilattice[ValueNilness] = lattice{}
// Equals implements [dfa.Semilattice].
func (l lattice) Equals(a, b ValueNilness) bool {
return a == b
}
// Ident implements [dfa.Semilattice].
func (l lattice) Ident() ValueNilness {
return ValueNilness{}
}
var latticeMerge = [5][5]Nilness{
0: {
0: 0,
NeverNil: NeverNil,
AlwaysNil: AlwaysNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
NeverNil: {
0: NeverNil,
NeverNil: NeverNil,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
AlwaysNil: {
0: AlwaysNil,
NeverNil: MaybeNil,
AlwaysNil: AlwaysNil,
MaybeNilGlobal: MaybeNil,
MaybeNil: MaybeNil,
},
MaybeNilGlobal: {
0: MaybeNilGlobal,
NeverNil: MaybeNilGlobal,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNilGlobal,
MaybeNil: MaybeNil,
},
MaybeNil: {
0: MaybeNil,
NeverNil: MaybeNil,
AlwaysNil: MaybeNil,
MaybeNilGlobal: MaybeNil,
MaybeNil: MaybeNil,
},
}
// Merge implements [dfa.Semilattice].
func (l lattice) Merge(a, b ValueNilness) ValueNilness {
return ValueNilness{
Inner: latticeMerge[a.Inner][b.Inner],
Outer: latticeMerge[a.Outer][b.Outer],
}
}
type numbering map[ir.Value]int
func (n numbering) number(v ir.Value) int {
i, ok := n[v]
if !ok {
i = len(n)
n[v] = i
}
return i
}