sa4005.go raw

   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