github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4005/sa4005.go (about)

     1  package sa4005
     2  
     3  import (
     4  	"fmt"
     5  	"go/types"
     6  
     7  	"github.com/amarpal/go-tools/analysis/lint"
     8  	"github.com/amarpal/go-tools/analysis/report"
     9  	"github.com/amarpal/go-tools/go/ir"
    10  	"github.com/amarpal/go-tools/go/ir/irutil"
    11  	"github.com/amarpal/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.Documentation{
    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().(*types.Named).Obj().Name(), fieldName))
   134  			}
   135  		}
   136  	}
   137  	return nil, nil
   138  }