| 1 | // Copyright 2021 The Go Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | // Package unusedwrite checks for unused writes to the elements of a struct or array object. |
| 6 | package unusedwrite |
| 7 | |
| 8 | import ( |
| 9 | "fmt" |
| 10 | "go/types" |
| 11 | |
| 12 | "golang.org/x/tools/go/analysis" |
| 13 | "golang.org/x/tools/go/analysis/passes/buildssa" |
| 14 | "golang.org/x/tools/go/ssa" |
| 15 | ) |
| 16 | |
| 17 | // Doc is a documentation string. |
| 18 | const Doc = `checks for unused writes |
| 19 | |
| 20 | The analyzer reports instances of writes to struct fields and |
| 21 | arrays that are never read. Specifically, when a struct object |
| 22 | or an array is copied, its elements are copied implicitly by |
| 23 | the compiler, and any element write to this copy does nothing |
| 24 | with the original object. |
| 25 | |
| 26 | For example: |
| 27 | |
| 28 | type T struct { x int } |
| 29 | func f(input []T) { |
| 30 | for i, v := range input { // v is a copy |
| 31 | v.x = i // unused write to field x |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | Another example is about non-pointer receiver: |
| 36 | |
| 37 | type T struct { x int } |
| 38 | func (t T) f() { // t is a copy |
| 39 | t.x = i // unused write to field x |
| 40 | } |
| 41 | ` |
| 42 | |
| 43 | // Analyzer reports instances of writes to struct fields and arrays |
| 44 | // that are never read. |
| 45 | var Analyzer = &analysis.Analyzer{ |
| 46 | Name: "unusedwrite", |
| 47 | Doc: Doc, |
| 48 | Requires: []*analysis.Analyzer{buildssa.Analyzer}, |
| 49 | Run: run, |
| 50 | } |
| 51 | |
| 52 | func run(pass *analysis.Pass) (interface{}, error) { |
| 53 | ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) |
| 54 | for _, fn := range ssainput.SrcFuncs { |
| 55 | // TODO(taking): Iterate over fn._Instantiations() once exported. If so, have 1 report per Pos(). |
| 56 | reports := checkStores(fn) |
| 57 | for _, store := range reports { |
| 58 | switch addr := store.Addr.(type) { |
| 59 | case *ssa.FieldAddr: |
| 60 | pass.Reportf(store.Pos(), |
| 61 | "unused write to field %s", |
| 62 | getFieldName(addr.X.Type(), addr.Field)) |
| 63 | case *ssa.IndexAddr: |
| 64 | pass.Reportf(store.Pos(), |
| 65 | "unused write to array index %s", addr.Index) |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | return nil, nil |
| 70 | } |
| 71 | |
| 72 | // checkStores returns *Stores in fn whose address is written to but never used. |
| 73 | func checkStores(fn *ssa.Function) []*ssa.Store { |
| 74 | var reports []*ssa.Store |
| 75 | // Visit each block. No need to visit fn.Recover. |
| 76 | for _, blk := range fn.Blocks { |
| 77 | for _, instr := range blk.Instrs { |
| 78 | // Identify writes. |
| 79 | if store, ok := instr.(*ssa.Store); ok { |
| 80 | // Consider field/index writes to an object whose elements are copied and not shared. |
| 81 | // MapUpdate is excluded since only the reference of the map is copied. |
| 82 | switch addr := store.Addr.(type) { |
| 83 | case *ssa.FieldAddr: |
| 84 | if isDeadStore(store, addr.X, addr) { |
| 85 | reports = append(reports, store) |
| 86 | } |
| 87 | case *ssa.IndexAddr: |
| 88 | if isDeadStore(store, addr.X, addr) { |
| 89 | reports = append(reports, store) |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | return reports |
| 96 | } |
| 97 | |
| 98 | // isDeadStore determines whether a field/index write to an object is dead. |
| 99 | // Argument "obj" is the object, and "addr" is the instruction fetching the field/index. |
| 100 | func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool { |
| 101 | // Consider only struct or array objects. |
| 102 | if !hasStructOrArrayType(obj) { |
| 103 | return false |
| 104 | } |
| 105 | // Check liveness: if the value is used later, then don't report the write. |
| 106 | for _, ref := range *obj.Referrers() { |
| 107 | if ref == store || ref == addr { |
| 108 | continue |
| 109 | } |
| 110 | switch ins := ref.(type) { |
| 111 | case ssa.CallInstruction: |
| 112 | return false |
| 113 | case *ssa.FieldAddr: |
| 114 | // Check whether the same field is used. |
| 115 | if ins.X == obj { |
| 116 | if faddr, ok := addr.(*ssa.FieldAddr); ok { |
| 117 | if faddr.Field == ins.Field { |
| 118 | return false |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | // Otherwise another field is used, and this usage doesn't count. |
| 123 | continue |
| 124 | case *ssa.IndexAddr: |
| 125 | if ins.X == obj { |
| 126 | return false |
| 127 | } |
| 128 | continue // Otherwise another object is used |
| 129 | case *ssa.Lookup: |
| 130 | if ins.X == obj { |
| 131 | return false |
| 132 | } |
| 133 | continue // Otherwise another object is used |
| 134 | case *ssa.Store: |
| 135 | if ins.Val == obj { |
| 136 | return false |
| 137 | } |
| 138 | continue // Otherwise other object is stored |
| 139 | default: // consider live if the object is used in any other instruction |
| 140 | return false |
| 141 | } |
| 142 | } |
| 143 | return true |
| 144 | } |
| 145 | |
| 146 | // isStructOrArray returns whether the underlying type is struct or array. |
| 147 | func isStructOrArray(tp types.Type) bool { |
| 148 | if named, ok := tp.(*types.Named); ok { |
| 149 | tp = named.Underlying() |
| 150 | } |
| 151 | switch tp.(type) { |
| 152 | case *types.Array: |
| 153 | return true |
| 154 | case *types.Struct: |
| 155 | return true |
| 156 | } |
| 157 | return false |
| 158 | } |
| 159 | |
| 160 | // hasStructOrArrayType returns whether a value is of struct or array type. |
| 161 | func hasStructOrArrayType(v ssa.Value) bool { |
| 162 | if instr, ok := v.(ssa.Instruction); ok { |
| 163 | if alloc, ok := instr.(*ssa.Alloc); ok { |
| 164 | // Check the element type of an allocated register (which always has pointer type) |
| 165 | // e.g., for |
| 166 | // func (t T) f() { ...} |
| 167 | // the receiver object is of type *T: |
| 168 | // t0 = local T (t) *T |
| 169 | if tp, ok := alloc.Type().(*types.Pointer); ok { |
| 170 | return isStructOrArray(tp.Elem()) |
| 171 | } |
| 172 | return false |
| 173 | } |
| 174 | } |
| 175 | return isStructOrArray(v.Type()) |
| 176 | } |
| 177 | |
| 178 | // getFieldName returns the name of a field in a struct. |
| 179 | // It the field is not found, then it returns the string format of the index. |
| 180 | // |
| 181 | // For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y". |
| 182 | func getFieldName(tp types.Type, index int) string { |
| 183 | if pt, ok := tp.(*types.Pointer); ok { |
| 184 | tp = pt.Elem() |
| 185 | } |
| 186 | if named, ok := tp.(*types.Named); ok { |
| 187 | tp = named.Underlying() |
| 188 | } |
| 189 | if stp, ok := tp.(*types.Struct); ok { |
| 190 | return stp.Field(index).Name() |
| 191 | } |
| 192 | return fmt.Sprintf("%d", index) |
| 193 | } |
| 194 |
Members