Skip to content

Commit ff6ba78

Browse files
committed
feat(slice): enhance extractLenBound to support additional offset patterns and improve slice bounds analysis
1 parent 73a61c4 commit ff6ba78

2 files changed

Lines changed: 101 additions & 20 deletions

File tree

analyzers/slice_bounds.go

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -246,38 +246,100 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
246246
return nil, nil
247247
}
248248

249-
// extractLenBound checks if the binop is of form "Var < Len + Offset"
249+
// extractLenBound checks if the binop is of form "Var < Len + Offset" or equivalent patterns
250+
// (including offsets on the left-hand side like "(Var + Const) < Len")
250251
func extractLenBound(binop *ssa.BinOp) (ssa.Value, int, bool) {
251252
// Only handle Less Than for now
252253
if binop.Op != token.LSS {
253254
return nil, 0, false
254255
}
255256

256-
// Assume LHS is the loop variable
257-
loopVar := binop.X
257+
var loopVar ssa.Value
258+
var lenOffset int
258259

259-
// Check RHS. It is either the Len instruction itself, or a BinOp (Len +/- Const)
260-
rhs := binop.Y
260+
// First, try to interpret RHS as the length expression (len +/- const) and LHS as plain loop var
261+
loopVar = binop.X // candidate loop variable
261262

262-
if _, ok := rhs.(*ssa.Const); !ok {
263-
if rOp, ok := rhs.(*ssa.BinOp); ok {
264-
// Assume Len is on the left of the arithmetic
265-
if c, ok := rOp.Y.(*ssa.Const); ok {
266-
val, err := strconv.Atoi(c.Value.String())
267-
if err == nil {
268-
switch rOp.Op {
269-
case token.ADD:
270-
return loopVar, val, true
271-
case token.SUB:
272-
return loopVar, -val, true
273-
}
263+
if _, isConst := binop.Y.(*ssa.Const); isConst {
264+
// RHS is a constant → cannot be a length-bound check
265+
return nil, 0, false
266+
}
267+
268+
// Try to pull an offset from RHS if it is len +/- const
269+
if rhsBinOp, ok := binop.Y.(*ssa.BinOp); ok && (rhsBinOp.Op == token.ADD || rhsBinOp.Op == token.SUB) {
270+
var constVal int
271+
var foundConst bool
272+
273+
// Check both sides for the constant (symmetric for ADD, careful for SUB)
274+
if c, ok := rhsBinOp.Y.(*ssa.Const); ok {
275+
if v, err := strconv.Atoi(c.Value.String()); err == nil {
276+
constVal = v
277+
foundConst = true
278+
}
279+
} else if c, ok := rhsBinOp.X.(*ssa.Const); ok {
280+
if v, err := strconv.Atoi(c.Value.String()); err == nil {
281+
constVal = v
282+
foundConst = true
283+
}
284+
}
285+
286+
if foundConst {
287+
switch rhsBinOp.Op {
288+
case token.ADD:
289+
// len + k or k + len → same meaning
290+
lenOffset = constVal
291+
case token.SUB:
292+
if _, isConstOnLeft := rhsBinOp.X.(*ssa.Const); isConstOnLeft {
293+
// k - len → unusual for a strict upper bound, skip this pattern
294+
foundConst = false
295+
} else {
296+
// len - k
297+
lenOffset = -constVal
274298
}
275299
}
300+
if foundConst {
301+
return loopVar, lenOffset, true
302+
}
276303
}
277-
// If it's not a BinOp but just a value (result of len call), offset is 0
278-
return loopVar, 0, true
279304
}
280-
return nil, 0, false
305+
306+
// If we get here, RHS is a plain length (no extractable offset) or extraction failed.
307+
// Now try the alternative pattern: LHS is (loopVar +/- const), RHS is plain len
308+
if lhsBinOp, ok := binop.X.(*ssa.BinOp); ok && (lhsBinOp.Op == token.ADD || lhsBinOp.Op == token.SUB) {
309+
var constVal int
310+
var varVal ssa.Value
311+
var found bool
312+
313+
if c, ok := lhsBinOp.Y.(*ssa.Const); ok {
314+
if v, err := strconv.Atoi(c.Value.String()); err == nil {
315+
constVal = v
316+
varVal = lhsBinOp.X
317+
found = true
318+
}
319+
} else if c, ok := lhsBinOp.X.(*ssa.Const); ok {
320+
if v, err := strconv.Atoi(c.Value.String()); err == nil {
321+
constVal = v
322+
varVal = lhsBinOp.Y
323+
found = true
324+
}
325+
}
326+
327+
if found {
328+
loopVar = varVal
329+
switch lhsBinOp.Op {
330+
case token.ADD:
331+
// (i + k) < len → equivalent to i < len - k
332+
lenOffset = -constVal
333+
case token.SUB:
334+
// (i - k) < len → equivalent to i < len + k (rare but safe)
335+
lenOffset = constVal
336+
}
337+
return loopVar, lenOffset, true
338+
}
339+
}
340+
341+
// Fallback: plain i < len (offset 0)
342+
return loopVar, 0, true
281343
}
282344

283345
// extractIndexOffset checks if indexVal is "loopVar + C"

testutils/g602_samples.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,5 +499,24 @@ func main() {
499499
safeTriples([]int{1,2,3,4,5,6,7})
500500
safeTriples([]int{1,2,3,4,5})
501501
}
502+
`}, 0, gosec.NewConfig()},
503+
{[]string{`
504+
package main
505+
506+
import "fmt"
507+
508+
func pairwise(list []any) {
509+
for i := 0; i+1 < len(list); i += 2 {
510+
// Safe: i+1 < len implies i < len-1
511+
fmt.Printf("%v %v\n", list[i], list[i+1])
512+
}
513+
}
514+
515+
func main() {
516+
// Calls with both even and odd lengths (and empty) to exercise the path
517+
pairwise([]any{"a", "b", "c", "d"})
518+
pairwise([]any{"x", "y", "z"})
519+
pairwise([]any{})
520+
}
502521
`}, 0, gosec.NewConfig()},
503522
}

0 commit comments

Comments
 (0)