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