src

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

sa4010.go (5650B)


      1 package sa4010
      2 
      3 import (
      4 	"honnef.co/go/tools/analysis/lint"
      5 	"honnef.co/go/tools/analysis/report"
      6 	"honnef.co/go/tools/go/ir"
      7 	"honnef.co/go/tools/internal/passes/buildir"
      8 
      9 	"golang.org/x/tools/go/analysis"
     10 )
     11 
     12 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
     13 	Analyzer: &analysis.Analyzer{
     14 		Name:     "SA4010",
     15 		Run:      run,
     16 		Requires: []*analysis.Analyzer{buildir.Analyzer},
     17 	},
     18 	Doc: &lint.RawDocumentation{
     19 		Title: `The result of \'append\' will never be observed anywhere`,
     20 		Text: `Calls to \'append\' produce a new slice value. When the result of
     21 \'append\' is assigned to a variable that is never subsequently read, the
     22 append operation may have an unintended effect.`,
     23 		Since:    "2017.1",
     24 		Severity: lint.SeverityWarning,
     25 		MergeIf:  lint.MergeIfAll,
     26 	},
     27 })
     28 
     29 var Analyzer = SCAnalyzer.Analyzer
     30 
     31 func run(pass *analysis.Pass) (any, error) {
     32 	isAppend := func(ins ir.Value) bool {
     33 		call, ok := ins.(*ir.Call)
     34 		if !ok {
     35 			return false
     36 		}
     37 		if call.Call.IsInvoke() {
     38 			return false
     39 		}
     40 		if builtin, ok := call.Call.Value.(*ir.Builtin); !ok || builtin.Name() != "append" {
     41 			return false
     42 		}
     43 		return true
     44 	}
     45 
     46 	// We have to be careful about aliasing.
     47 	// Multiple slices may refer to the same backing array,
     48 	// making appends observable even when we don't see the result of append be used anywhere.
     49 	//
     50 	// We will have to restrict ourselves to slices that have been allocated within the function,
     51 	// haven't been sliced,
     52 	// and haven't been passed anywhere that could retain them (such as function calls or memory stores).
     53 	//
     54 	// We check whether an append should be flagged in two steps.
     55 	//
     56 	// In the first step, we look at the data flow graph, starting in reverse from the argument to append, till we reach the root.
     57 	// This graph must only consist of the following instructions:
     58 	//
     59 	// - phi
     60 	// - slice
     61 	// - const nil
     62 	// - MakeSlice
     63 	// - Alloc
     64 	// - calls to append
     65 	//
     66 	// If this step succeeds, we look at all referrers of the values found in the first step, recursively.
     67 	// These referrers must either be in the set of values found in the first step
     68 	// or fulfill the same type requirements as step 1, with the exception of appends, which are forbidden.
     69 	//
     70 	// If both steps succeed then we know that the backing array hasn't been aliased in an observable manner.
     71 	//
     72 	// We could relax these restrictions by making use of additional information:
     73 	// - if we passed the slice to a function that doesn't retain the slice then we can still flag it
     74 	// - if a slice has been sliced but is dead afterwards, we can flag appends to the new slice
     75 
     76 	// OPT(dh): We could cache the results of both validate functions.
     77 	// However, we only use these functions on values that we otherwise want to flag, which are very few.
     78 	// Not caching values hasn't increased the runtimes for the standard library nor k8s.
     79 	var validateArgument func(v ir.Value, seen map[ir.Value]struct{}) bool
     80 	validateArgument = func(v ir.Value, seen map[ir.Value]struct{}) bool {
     81 		if _, ok := seen[v]; ok {
     82 			// break cycle
     83 			return true
     84 		}
     85 		seen[v] = struct{}{}
     86 		switch v := v.(type) {
     87 		case *ir.Phi:
     88 			for _, edge := range v.Edges {
     89 				if !validateArgument(edge, seen) {
     90 					return false
     91 				}
     92 			}
     93 			return true
     94 		case *ir.Slice:
     95 			return validateArgument(v.X, seen)
     96 		case *ir.Const:
     97 			return true
     98 		case *ir.MakeSlice:
     99 			return true
    100 		case *ir.Alloc:
    101 			return true
    102 		case *ir.Call:
    103 			if isAppend(v) {
    104 				return validateArgument(v.Call.Args[0], seen)
    105 			}
    106 			return false
    107 		default:
    108 			return false
    109 		}
    110 	}
    111 
    112 	var validateReferrers func(v ir.Value, seen map[ir.Instruction]struct{}) bool
    113 	validateReferrers = func(v ir.Value, seen map[ir.Instruction]struct{}) bool {
    114 		if refs := v.Referrers(); refs != nil {
    115 			for _, ref := range *refs {
    116 				if _, ok := seen[ref]; ok {
    117 					continue
    118 				}
    119 
    120 				seen[ref] = struct{}{}
    121 				switch ref.(type) {
    122 				case *ir.Phi:
    123 				case *ir.Slice:
    124 				case *ir.MakeSlice:
    125 				case *ir.Alloc:
    126 				default:
    127 					return false
    128 				}
    129 
    130 				if ref, ok := ref.(ir.Value); ok {
    131 					if !validateReferrers(ref, seen) {
    132 						return false
    133 					}
    134 				}
    135 			}
    136 		}
    137 		return true
    138 	}
    139 
    140 	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
    141 		for _, block := range fn.Blocks {
    142 			for _, ins := range block.Instrs {
    143 				val, ok := ins.(ir.Value)
    144 				if !ok || !isAppend(val) {
    145 					continue
    146 				}
    147 
    148 				isUsed := false
    149 				visited := map[ir.Instruction]bool{}
    150 				var walkRefs func(refs []ir.Instruction)
    151 				walkRefs = func(refs []ir.Instruction) {
    152 				loop:
    153 					for _, ref := range refs {
    154 						if visited[ref] {
    155 							continue
    156 						}
    157 						visited[ref] = true
    158 						switch ref := ref.(type) {
    159 						case *ir.Phi:
    160 							walkRefs(*ref.Referrers())
    161 						case ir.Value:
    162 							if !isAppend(ref) {
    163 								isUsed = true
    164 							} else {
    165 								walkRefs(*ref.Referrers())
    166 							}
    167 						case ir.Instruction:
    168 							isUsed = true
    169 							break loop
    170 						}
    171 					}
    172 				}
    173 
    174 				refs := val.Referrers()
    175 				if refs == nil {
    176 					continue
    177 				}
    178 				walkRefs(*refs)
    179 
    180 				if isUsed {
    181 					continue
    182 				}
    183 
    184 				seen := map[ir.Value]struct{}{}
    185 				if !validateArgument(ins.(*ir.Call).Call.Args[0], seen) {
    186 					continue
    187 				}
    188 
    189 				seen2 := map[ir.Instruction]struct{}{}
    190 				for k := range seen {
    191 					if k, ok := k.(ir.Instruction); ok {
    192 						seen2[k] = struct{}{}
    193 					}
    194 				}
    195 				seen2[ins] = struct{}{}
    196 				failed := false
    197 				for v := range seen {
    198 					if !validateReferrers(v, seen2) {
    199 						failed = true
    200 						break
    201 					}
    202 				}
    203 				if !failed {
    204 					report.Report(pass, ins, "this result of append is never used, except maybe in other appends")
    205 				}
    206 			}
    207 		}
    208 	}
    209 	return nil, nil
    210 }