|
| 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 | + // Check the writes to struct and array objects. |
| 54 | + checkStore := func(store *ssa.Store) { |
| 55 | + // Consider field/index writes to an object whose elements are copied and not shared. |
| 56 | + // MapUpdate is excluded since only the reference of the map is copied. |
| 57 | + switch addr := store.Addr.(type) { |
| 58 | + case *ssa.FieldAddr: |
| 59 | + if isDeadStore(store, addr.X, addr) { |
| 60 | + // Report the bug. |
| 61 | + pass.Reportf(store.Pos(), |
| 62 | + "unused write to field %s", |
| 63 | + getFieldName(addr.X.Type(), addr.Field)) |
| 64 | + } |
| 65 | + case *ssa.IndexAddr: |
| 66 | + if isDeadStore(store, addr.X, addr) { |
| 67 | + // Report the bug. |
| 68 | + pass.Reportf(store.Pos(), |
| 69 | + "unused write to array index %s", addr.Index) |
| 70 | + } |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) |
| 75 | + for _, fn := range ssainput.SrcFuncs { |
| 76 | + // Visit each block. No need to visit fn.Recover. |
| 77 | + for _, blk := range fn.Blocks { |
| 78 | + for _, instr := range blk.Instrs { |
| 79 | + // Identify writes. |
| 80 | + if store, ok := instr.(*ssa.Store); ok { |
| 81 | + checkStore(store) |
| 82 | + } |
| 83 | + } |
| 84 | + } |
| 85 | + } |
| 86 | + return nil, nil |
| 87 | +} |
| 88 | + |
| 89 | +// isDeadStore determines whether a field/index write to an object is dead. |
| 90 | +// Argument "obj" is the object, and "addr" is the instruction fetching the field/index. |
| 91 | +func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool { |
| 92 | + // Consider only struct or array objects. |
| 93 | + if !hasStructOrArrayType(obj) { |
| 94 | + return false |
| 95 | + } |
| 96 | + // Check liveness: if the value is used later, then don't report the write. |
| 97 | + for _, ref := range *obj.Referrers() { |
| 98 | + if ref == store || ref == addr { |
| 99 | + continue |
| 100 | + } |
| 101 | + switch ins := ref.(type) { |
| 102 | + case ssa.CallInstruction: |
| 103 | + return false |
| 104 | + case *ssa.FieldAddr: |
| 105 | + // Check whether the same field is used. |
| 106 | + if ins.X == obj { |
| 107 | + if faddr, ok := addr.(*ssa.FieldAddr); ok { |
| 108 | + if faddr.Field == ins.Field { |
| 109 | + return false |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + // Otherwise another field is used, and this usage doesn't count. |
| 114 | + continue |
| 115 | + case *ssa.IndexAddr: |
| 116 | + if ins.X == obj { |
| 117 | + return false |
| 118 | + } |
| 119 | + continue // Otherwise another object is used |
| 120 | + case *ssa.Lookup: |
| 121 | + if ins.X == obj { |
| 122 | + return false |
| 123 | + } |
| 124 | + continue // Otherwise another object is used |
| 125 | + case *ssa.Store: |
| 126 | + if ins.Val == obj { |
| 127 | + return false |
| 128 | + } |
| 129 | + continue // Otherwise other object is stored |
| 130 | + default: // consider live if the object is used in any other instruction |
| 131 | + return false |
| 132 | + } |
| 133 | + } |
| 134 | + return true |
| 135 | +} |
| 136 | + |
| 137 | +// isStructOrArray returns whether the underlying type is struct or array. |
| 138 | +func isStructOrArray(tp types.Type) bool { |
| 139 | + if named, ok := tp.(*types.Named); ok { |
| 140 | + tp = named.Underlying() |
| 141 | + } |
| 142 | + switch tp.(type) { |
| 143 | + case *types.Array: |
| 144 | + return true |
| 145 | + case *types.Struct: |
| 146 | + return true |
| 147 | + } |
| 148 | + return false |
| 149 | +} |
| 150 | + |
| 151 | +// hasStructOrArrayType returns whether a value is of struct or array type. |
| 152 | +func hasStructOrArrayType(v ssa.Value) bool { |
| 153 | + if instr, ok := v.(ssa.Instruction); ok { |
| 154 | + if alloc, ok := instr.(*ssa.Alloc); ok { |
| 155 | + // Check the element type of an allocated register (which always has pointer type) |
| 156 | + // e.g., for |
| 157 | + // func (t T) f() { ...} |
| 158 | + // the receiver object is of type *T: |
| 159 | + // t0 = local T (t) *T |
| 160 | + if tp, ok := alloc.Type().(*types.Pointer); ok { |
| 161 | + return isStructOrArray(tp.Elem()) |
| 162 | + } |
| 163 | + return false |
| 164 | + } |
| 165 | + } |
| 166 | + return isStructOrArray(v.Type()) |
| 167 | +} |
| 168 | + |
| 169 | +// getFieldName returns the name of a field in a struct. |
| 170 | +// It the field is not found, then it returns the string format of the index. |
| 171 | +// |
| 172 | +// For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y". |
| 173 | +func getFieldName(tp types.Type, index int) string { |
| 174 | + if pt, ok := tp.(*types.Pointer); ok { |
| 175 | + tp = pt.Elem() |
| 176 | + } |
| 177 | + if named, ok := tp.(*types.Named); ok { |
| 178 | + tp = named.Underlying() |
| 179 | + } |
| 180 | + if stp, ok := tp.(*types.Struct); ok { |
| 181 | + return stp.Field(index).Name() |
| 182 | + } |
| 183 | + return fmt.Sprintf("%d", index) |
| 184 | +} |
0 commit comments