139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
package sa4005
|
|
|
|
import (
|
|
"fmt"
|
|
"go/types"
|
|
|
|
"honnef.co/go/tools/analysis/lint"
|
|
"honnef.co/go/tools/analysis/report"
|
|
"honnef.co/go/tools/go/ir"
|
|
"honnef.co/go/tools/go/ir/irutil"
|
|
"honnef.co/go/tools/internal/passes/buildir"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
|
|
Analyzer: &analysis.Analyzer{
|
|
Name: "SA4005",
|
|
Run: run,
|
|
Requires: []*analysis.Analyzer{buildir.Analyzer},
|
|
},
|
|
Doc: &lint.RawDocumentation{
|
|
Title: `Field assignment that will never be observed. Did you mean to use a pointer receiver?`,
|
|
Since: "2021.1",
|
|
Severity: lint.SeverityWarning,
|
|
MergeIf: lint.MergeIfAny,
|
|
},
|
|
})
|
|
|
|
var Analyzer = SCAnalyzer.Analyzer
|
|
|
|
func run(pass *analysis.Pass) (any, error) {
|
|
// The analysis only considers the receiver and its first level
|
|
// fields. It doesn't look at other parameters, nor at nested
|
|
// fields.
|
|
//
|
|
// The analysis does not detect all kinds of dead stores, only
|
|
// those of fields that are never read after the write. That is,
|
|
// we do not flag 'a.x = 1; a.x = 2; _ = a.x'. We might explore
|
|
// this again if we add support for SROA to go/ir and implement
|
|
// https://github.com/dominikh/go-tools/issues/191.
|
|
|
|
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
|
|
fnLoop:
|
|
for _, fn := range irpkg.SrcFuncs {
|
|
if recv := fn.Signature.Recv(); recv == nil {
|
|
continue
|
|
} else if _, ok := recv.Type().Underlying().(*types.Struct); !ok {
|
|
continue
|
|
}
|
|
|
|
recv := fn.Params[0]
|
|
refs := irutil.FilterDebug(*recv.Referrers())
|
|
if len(refs) != 1 {
|
|
continue
|
|
}
|
|
store, ok := refs[0].(*ir.Store)
|
|
if !ok {
|
|
continue
|
|
}
|
|
alloc, ok := store.Addr.(*ir.Alloc)
|
|
if !ok || alloc.Heap {
|
|
continue
|
|
}
|
|
|
|
reads := map[int][]ir.Instruction{}
|
|
writes := map[int][]ir.Instruction{}
|
|
for _, ref := range *alloc.Referrers() {
|
|
switch ref := ref.(type) {
|
|
case *ir.FieldAddr:
|
|
for _, refref := range *ref.Referrers() {
|
|
switch refref.(type) {
|
|
case *ir.Store:
|
|
writes[ref.Field] = append(writes[ref.Field], refref)
|
|
case *ir.Load:
|
|
reads[ref.Field] = append(reads[ref.Field], refref)
|
|
case *ir.DebugRef:
|
|
continue
|
|
default:
|
|
// this should be safe… if the field address
|
|
// escapes, then alloc.Heap will be true.
|
|
// there should be no instructions left that,
|
|
// given this FieldAddr, without escaping, can
|
|
// effect a load or store.
|
|
continue
|
|
}
|
|
}
|
|
case *ir.Store:
|
|
// we could treat this as a store to every field, but
|
|
// we don't want to decide the semantics of partial
|
|
// struct initializers. should `v = t{x: 1}` also mark
|
|
// v.y as being written to?
|
|
if ref != store {
|
|
continue fnLoop
|
|
}
|
|
case *ir.Load:
|
|
// a load of the entire struct loads every field
|
|
for i := 0; i < recv.Type().Underlying().(*types.Struct).NumFields(); i++ {
|
|
reads[i] = append(reads[i], ref)
|
|
}
|
|
case *ir.DebugRef:
|
|
continue
|
|
default:
|
|
continue fnLoop
|
|
}
|
|
}
|
|
|
|
offset := func(instr ir.Instruction) int {
|
|
for i, other := range instr.Block().Instrs {
|
|
if instr == other {
|
|
return i
|
|
}
|
|
}
|
|
panic("couldn't find instruction in its block")
|
|
}
|
|
|
|
for field, ws := range writes {
|
|
rs := reads[field]
|
|
wLoop:
|
|
for _, w := range ws {
|
|
for _, r := range rs {
|
|
if w.Block() == r.Block() {
|
|
if offset(r) > offset(w) {
|
|
// found a reachable read of our write
|
|
continue wLoop
|
|
}
|
|
} else if irutil.Reachable(w.Block(), r.Block()) {
|
|
// found a reachable read of our write
|
|
continue wLoop
|
|
}
|
|
}
|
|
fieldName := recv.Type().Underlying().(*types.Struct).Field(field).Name()
|
|
report.Report(pass, w, fmt.Sprintf("ineffective assignment to field %s.%s", recv.Type().(interface{ Obj() *types.TypeName }).Obj().Name(), fieldName))
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|