github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa5012/sa5012.go (about) 1 package sa5012 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/constant" 7 "go/token" 8 "go/types" 9 10 "github.com/amarpal/go-tools/analysis/lint" 11 "github.com/amarpal/go-tools/analysis/report" 12 "github.com/amarpal/go-tools/go/ir" 13 "github.com/amarpal/go-tools/go/ir/irutil" 14 "github.com/amarpal/go-tools/go/types/typeutil" 15 "github.com/amarpal/go-tools/internal/passes/buildir" 16 17 "golang.org/x/tools/go/analysis" 18 ) 19 20 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 21 Analyzer: &analysis.Analyzer{ 22 Name: "SA5012", 23 Run: run, 24 FactTypes: []analysis.Fact{new(evenElements)}, 25 Requires: []*analysis.Analyzer{buildir.Analyzer}, 26 }, 27 Doc: &lint.Documentation{ 28 Title: "Passing odd-sized slice to function expecting even size", 29 Text: `Some functions that take slices as parameters expect the slices to have an even number of elements. 30 Often, these functions treat elements in a slice as pairs. 31 For example, \'strings.NewReplacer\' takes pairs of old and new strings, 32 and calling it with an odd number of elements would be an error.`, 33 Since: "2020.2", 34 Severity: lint.SeverityError, 35 MergeIf: lint.MergeIfAny, 36 }, 37 }) 38 39 var Analyzer = SCAnalyzer.Analyzer 40 41 type evenElements struct{} 42 43 func (evenElements) AFact() {} 44 45 func (evenElements) String() string { return "needs even elements" } 46 47 func findSliceLength(v ir.Value) int { 48 // TODO(dh): VRP would help here 49 50 v = irutil.Flatten(v) 51 val := func(v ir.Value) int { 52 if v, ok := v.(*ir.Const); ok { 53 return int(v.Int64()) 54 } 55 return -1 56 } 57 switch v := v.(type) { 58 case *ir.Slice: 59 low := 0 60 high := -1 61 if v.Low != nil { 62 low = val(v.Low) 63 } 64 if v.High != nil { 65 high = val(v.High) 66 } else { 67 switch vv := v.X.(type) { 68 case *ir.Alloc: 69 high = int(typeutil.Dereference(vv.Type()).Underlying().(*types.Array).Len()) 70 case *ir.Slice: 71 high = findSliceLength(vv) 72 } 73 } 74 if low == -1 || high == -1 { 75 return -1 76 } 77 return high - low 78 default: 79 return -1 80 } 81 } 82 83 func flagSliceLens(pass *analysis.Pass) { 84 var tag evenElements 85 86 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 87 for _, b := range fn.Blocks { 88 for _, instr := range b.Instrs { 89 call, ok := instr.(ir.CallInstruction) 90 if !ok { 91 continue 92 } 93 callee := call.Common().StaticCallee() 94 if callee == nil { 95 continue 96 } 97 for argi, arg := range call.Common().Args { 98 if callee.Signature.Recv() != nil { 99 if argi == 0 { 100 continue 101 } 102 argi-- 103 } 104 105 _, ok := arg.Type().Underlying().(*types.Slice) 106 if !ok { 107 continue 108 } 109 param := callee.Signature.Params().At(argi) 110 if !pass.ImportObjectFact(param, &tag) { 111 continue 112 } 113 114 // TODO handle stubs 115 116 // we know the argument has to have even length. 117 // now let's try to find its length 118 if n := findSliceLength(arg); n > -1 && n%2 != 0 { 119 src := call.Source().(*ast.CallExpr).Args[argi] 120 sig := call.Common().Signature() 121 var label string 122 if argi == sig.Params().Len()-1 && sig.Variadic() { 123 label = "variadic argument" 124 } else { 125 label = "argument" 126 } 127 // Note that param.Name() is guaranteed to not 128 // be empty, otherwise the function couldn't 129 // have enforced its length. 130 report.Report(pass, src, fmt.Sprintf("%s %q is expected to have even number of elements, but has %d elements", label, param.Name(), n)) 131 } 132 } 133 } 134 } 135 } 136 } 137 138 func findSliceLenChecks(pass *analysis.Pass) { 139 // mark all function parameters that have to be of even length 140 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 141 for _, b := range fn.Blocks { 142 // all paths go through this block 143 if !b.Dominates(fn.Exit) { 144 continue 145 } 146 147 // if foo % 2 != 0 148 ifi, ok := b.Control().(*ir.If) 149 if !ok { 150 continue 151 } 152 cmp, ok := ifi.Cond.(*ir.BinOp) 153 if !ok { 154 continue 155 } 156 var needle uint64 157 switch cmp.Op { 158 case token.NEQ: 159 // look for != 0 160 needle = 0 161 case token.EQL: 162 // look for == 1 163 needle = 1 164 default: 165 continue 166 } 167 168 rem, ok1 := cmp.X.(*ir.BinOp) 169 k, ok2 := cmp.Y.(*ir.Const) 170 if ok1 != ok2 { 171 continue 172 } 173 if !ok1 { 174 rem, ok1 = cmp.Y.(*ir.BinOp) 175 k, ok2 = cmp.X.(*ir.Const) 176 } 177 if !ok1 || !ok2 || rem.Op != token.REM || k.Value.Kind() != constant.Int || k.Uint64() != needle { 178 continue 179 } 180 k, ok = rem.Y.(*ir.Const) 181 if !ok || k.Value.Kind() != constant.Int || k.Uint64() != 2 { 182 continue 183 } 184 185 // if len(foo) % 2 != 0 186 call, ok := rem.X.(*ir.Call) 187 if !ok || !irutil.IsCallTo(call.Common(), "len") { 188 continue 189 } 190 191 // we're checking the length of a parameter that is a slice 192 // TODO(dh): support parameters that have flown through sigmas and phis 193 param, ok := call.Call.Args[0].(*ir.Parameter) 194 if !ok { 195 continue 196 } 197 if !typeutil.All(param.Type(), typeutil.IsSlice) { 198 continue 199 } 200 201 // if len(foo) % 2 != 0 then panic 202 if _, ok := b.Succs[0].Control().(*ir.Panic); !ok { 203 continue 204 } 205 206 pass.ExportObjectFact(param.Object(), new(evenElements)) 207 } 208 } 209 } 210 211 func findIndirectSliceLenChecks(pass *analysis.Pass) { 212 seen := map[*ir.Function]struct{}{} 213 214 var doFunction func(fn *ir.Function) 215 doFunction = func(fn *ir.Function) { 216 if _, ok := seen[fn]; ok { 217 return 218 } 219 seen[fn] = struct{}{} 220 221 for _, b := range fn.Blocks { 222 // all paths go through this block 223 if !b.Dominates(fn.Exit) { 224 continue 225 } 226 227 for _, instr := range b.Instrs { 228 call, ok := instr.(*ir.Call) 229 if !ok { 230 continue 231 } 232 callee := call.Call.StaticCallee() 233 if callee == nil { 234 continue 235 } 236 237 if callee.Pkg == fn.Pkg || callee.Pkg == nil { 238 doFunction(callee) 239 } 240 241 for argi, arg := range call.Call.Args { 242 if callee.Signature.Recv() != nil { 243 if argi == 0 { 244 continue 245 } 246 argi-- 247 } 248 249 // TODO(dh): support parameters that have flown through length-preserving instructions 250 param, ok := arg.(*ir.Parameter) 251 if !ok { 252 continue 253 } 254 if !typeutil.All(param.Type(), typeutil.IsSlice) { 255 continue 256 } 257 258 // We can't use callee.Params to look up the 259 // parameter, because Params is not populated for 260 // external functions. In our modular analysis. 261 // any function in any package that isn't the 262 // current package is considered "external", as it 263 // has been loaded from export data only. 264 sigParams := callee.Signature.Params() 265 266 if !pass.ImportObjectFact(sigParams.At(argi), new(evenElements)) { 267 continue 268 } 269 pass.ExportObjectFact(param.Object(), new(evenElements)) 270 } 271 } 272 } 273 } 274 275 for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs { 276 doFunction(fn) 277 } 278 } 279 280 func run(pass *analysis.Pass) (interface{}, error) { 281 findSliceLenChecks(pass) 282 findIndirectSliceLenChecks(pass) 283 flagSliceLens(pass) 284 285 return nil, nil 286 }