src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

sa4005.go (3744B)


      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) (any, 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 := *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 					default:
     78 						// this should be safe… if the field address
     79 						// escapes, then alloc.Heap will be true.
     80 						// there should be no instructions left that,
     81 						// given this FieldAddr, without escaping, can
     82 						// effect a load or store.
     83 						continue
     84 					}
     85 				}
     86 			case *ir.Store:
     87 				// we could treat this as a store to every field, but
     88 				// we don't want to decide the semantics of partial
     89 				// struct initializers. should `v = t{x: 1}` also mark
     90 				// v.y as being written to?
     91 				if ref != store {
     92 					continue fnLoop
     93 				}
     94 			case *ir.Load:
     95 				// a load of the entire struct loads every field
     96 				for i := 0; i < recv.Type().Underlying().(*types.Struct).NumFields(); i++ {
     97 					reads[i] = append(reads[i], ref)
     98 				}
     99 			default:
    100 				continue fnLoop
    101 			}
    102 		}
    103 
    104 		offset := func(instr ir.Instruction) int {
    105 			for i, other := range instr.Block().Instrs {
    106 				if instr == other {
    107 					return i
    108 				}
    109 			}
    110 			panic("couldn't find instruction in its block")
    111 		}
    112 
    113 		for field, ws := range writes {
    114 			rs := reads[field]
    115 		wLoop:
    116 			for _, w := range ws {
    117 				for _, r := range rs {
    118 					if w.Block() == r.Block() {
    119 						if offset(r) > offset(w) {
    120 							// found a reachable read of our write
    121 							continue wLoop
    122 						}
    123 					} else if irutil.Reachable(w.Block(), r.Block()) {
    124 						// found a reachable read of our write
    125 						continue wLoop
    126 					}
    127 				}
    128 				fieldName := recv.Type().Underlying().(*types.Struct).Field(field).Name()
    129 				report.Report(pass, w, fmt.Sprintf("ineffective assignment to field %s.%s", recv.Type().(interface{ Obj() *types.TypeName }).Obj().Name(), fieldName))
    130 			}
    131 		}
    132 	}
    133 	return nil, nil
    134 }