sa4005.go (3744B)
1 package sa4005 2 3 import ( 4 "fmt" 5 "go/types" 6 7 "honnef.co/go/tools/analysis/lint" 8 "honnef.co/go/tools/analysis/report" 9 "honnef.co/go/tools/go/ir" 10 "honnef.co/go/tools/go/ir/irutil" 11 "honnef.co/go/tools/internal/passes/buildir" 12 13 "golang.org/x/tools/go/analysis" 14 ) 15 16 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 17 Analyzer: &analysis.Analyzer{ 18 Name: "SA4005", 19 Run: run, 20 Requires: []*analysis.Analyzer{buildir.Analyzer}, 21 }, 22 Doc: &lint.RawDocumentation{ 23 Title: `Field assignment that will never be observed. Did you mean to use a pointer receiver?`, 24 Since: "2021.1", 25 Severity: lint.SeverityWarning, 26 MergeIf: lint.MergeIfAny, 27 }, 28 }) 29 30 var Analyzer = SCAnalyzer.Analyzer 31 32 func run(pass *analysis.Pass) (any, error) { 33 // The analysis only considers the receiver and its first level 34 // fields. It doesn't look at other parameters, nor at nested 35 // fields. 36 // 37 // The analysis does not detect all kinds of dead stores, only 38 // those of fields that are never read after the write. That is, 39 // we do not flag 'a.x = 1; a.x = 2; _ = a.x'. We might explore 40 // this again if we add support for SROA to go/ir and implement 41 // https://github.com/dominikh/go-tools/issues/191. 42 43 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR) 44 fnLoop: 45 for _, fn := range irpkg.SrcFuncs { 46 if recv := fn.Signature.Recv(); recv == nil { 47 continue 48 } else if _, ok := recv.Type().Underlying().(*types.Struct); !ok { 49 continue 50 } 51 52 recv := fn.Params[0] 53 refs := *recv.Referrers() 54 if len(refs) != 1 { 55 continue 56 } 57 store, ok := refs[0].(*ir.Store) 58 if !ok { 59 continue 60 } 61 alloc, ok := store.Addr.(*ir.Alloc) 62 if !ok || alloc.Heap { 63 continue 64 } 65 66 reads := map[int][]ir.Instruction{} 67 writes := map[int][]ir.Instruction{} 68 for _, ref := range *alloc.Referrers() { 69 switch ref := ref.(type) { 70 case *ir.FieldAddr: 71 for _, refref := range *ref.Referrers() { 72 switch refref.(type) { 73 case *ir.Store: 74 writes[ref.Field] = append(writes[ref.Field], refref) 75 case *ir.Load: 76 reads[ref.Field] = append(reads[ref.Field], refref) 77 default: 78 // this should be safe⦠if the field address 79 // escapes, then alloc.Heap will be true. 80 // there should be no instructions left that, 81 // given this FieldAddr, without escaping, can 82 // effect a load or store. 83 continue 84 } 85 } 86 case *ir.Store: 87 // we could treat this as a store to every field, but 88 // we don't want to decide the semantics of partial 89 // struct initializers. should `v = t{x: 1}` also mark 90 // v.y as being written to? 91 if ref != store { 92 continue fnLoop 93 } 94 case *ir.Load: 95 // a load of the entire struct loads every field 96 for i := 0; i < recv.Type().Underlying().(*types.Struct).NumFields(); i++ { 97 reads[i] = append(reads[i], ref) 98 } 99 default: 100 continue fnLoop 101 } 102 } 103 104 offset := func(instr ir.Instruction) int { 105 for i, other := range instr.Block().Instrs { 106 if instr == other { 107 return i 108 } 109 } 110 panic("couldn't find instruction in its block") 111 } 112 113 for field, ws := range writes { 114 rs := reads[field] 115 wLoop: 116 for _, w := range ws { 117 for _, r := range rs { 118 if w.Block() == r.Block() { 119 if offset(r) > offset(w) { 120 // found a reachable read of our write 121 continue wLoop 122 } 123 } else if irutil.Reachable(w.Block(), r.Block()) { 124 // found a reachable read of our write 125 continue wLoop 126 } 127 } 128 fieldName := recv.Type().Underlying().(*types.Struct).Field(field).Name() 129 report.Report(pass, w, fmt.Sprintf("ineffective assignment to field %s.%s", recv.Type().(interface{ Obj() *types.TypeName }).Obj().Name(), fieldName)) 130 } 131 } 132 } 133 return nil, nil 134 }