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 }