github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4005/sa4005.go (about) 1 package sa4005 2 3 import ( 4 "fmt" 5 "go/types" 6 7 "github.com/amarpal/go-tools/analysis/lint" 8 "github.com/amarpal/go-tools/analysis/report" 9 "github.com/amarpal/go-tools/go/ir" 10 "github.com/amarpal/go-tools/go/ir/irutil" 11 "github.com/amarpal/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.Documentation{ 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) (interface{}, 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 := irutil.FilterDebug(*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 case *ir.DebugRef: 78 continue 79 default: 80 // this should be safeā¦ if the field address 81 // escapes, then alloc.Heap will be true. 82 // there should be no instructions left that, 83 // given this FieldAddr, without escaping, can 84 // effect a load or store. 85 continue 86 } 87 } 88 case *ir.Store: 89 // we could treat this as a store to every field, but 90 // we don't want to decide the semantics of partial 91 // struct initializers. should `v = t{x: 1}` also mark 92 // v.y as being written to? 93 if ref != store { 94 continue fnLoop 95 } 96 case *ir.Load: 97 // a load of the entire struct loads every field 98 for i := 0; i < recv.Type().Underlying().(*types.Struct).NumFields(); i++ { 99 reads[i] = append(reads[i], ref) 100 } 101 case *ir.DebugRef: 102 continue 103 default: 104 continue fnLoop 105 } 106 } 107 108 offset := func(instr ir.Instruction) int { 109 for i, other := range instr.Block().Instrs { 110 if instr == other { 111 return i 112 } 113 } 114 panic("couldn't find instruction in its block") 115 } 116 117 for field, ws := range writes { 118 rs := reads[field] 119 wLoop: 120 for _, w := range ws { 121 for _, r := range rs { 122 if w.Block() == r.Block() { 123 if offset(r) > offset(w) { 124 // found a reachable read of our write 125 continue wLoop 126 } 127 } else if irutil.Reachable(w.Block(), r.Block()) { 128 // found a reachable read of our write 129 continue wLoop 130 } 131 } 132 fieldName := recv.Type().Underlying().(*types.Struct).Field(field).Name() 133 report.Report(pass, w, fmt.Sprintf("ineffective assignment to field %s.%s", recv.Type().(*types.Named).Obj().Name(), fieldName)) 134 } 135 } 136 } 137 return nil, nil 138 }