| // 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) { |
| // Check the writes to struct and array objects. |
| checkStore := func(store *ssa.Store) { |
| // 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) { |
| // Report the bug. |
| pass.Reportf(store.Pos(), |
| "unused write to field %s", |
| getFieldName(addr.X.Type(), addr.Field)) |
| } |
| case *ssa.IndexAddr: |
| if isDeadStore(store, addr.X, addr) { |
| // Report the bug. |
| pass.Reportf(store.Pos(), |
| "unused write to array index %s", addr.Index) |
| } |
| } |
| } |
| |
| ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) |
| for _, fn := range ssainput.SrcFuncs { |
| // 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 { |
| checkStore(store) |
| } |
| } |
| } |
| } |
| return nil, nil |
| } |
| |
| // 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) |
| } |