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) (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().(interface{ Obj() *types.TypeName }).Obj().Name(), fieldName))
134 }
135 }
136 }
137 return nil, nil
138 }
139