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