@@ -183,7 +183,7 @@ func stringscut(pass *analysis.Pass) (any, error) {
183183 // len(substr)]), then we can replace the call to Index()
184184 // with a call to Cut() and use the returned ok, before,
185185 // and after variables accordingly.
186- negative , nonnegative , beforeSlice , afterSlice := checkIdxUses (pass .TypesInfo , index .Uses (iObj ), s , substr )
186+ negative , nonnegative , beforeSlice , afterSlice := checkIdxUses (pass .TypesInfo , index .Uses (iObj ), s , substr , iObj )
187187
188188 // Either there are no uses of before, after, or ok, or some use
189189 // of i does not match our criteria - don't suggest a fix.
@@ -374,14 +374,31 @@ func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afte
374374// 2. nonnegative - a condition equivalent to i >= 0
375375// 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i]
376376// 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr))
377- func checkIdxUses (info * types.Info , uses iter.Seq [inspector.Cursor ], s , substr ast.Expr ) (negative , nonnegative , beforeSlice , afterSlice []ast.Expr ) {
377+ //
378+ // Additionally, all beforeSlice and afterSlice uses must be dominated by a
379+ // nonnegative guard on i (i.e., inside the body of an if whose condition
380+ // checks i >= 0, or in the else of a negative check, or after an
381+ // early-return negative check). This ensures that the rewrite from
382+ // s[i+len(sep):] to "after" preserves semantics, since when i == -1,
383+ // s[i+len(sep):] may yield a valid substring (e.g. s[0:] for single-byte
384+ // separators), but "after" would be "".
385+ //
386+ // When len(substr)==1, it's safe to use s[i+1:] even when i < 0.
387+ // Otherwise, each replacement of s[i+1:] must be guarded by a check
388+ // that i is nonnegative.
389+ func checkIdxUses (info * types.Info , uses iter.Seq [inspector.Cursor ], s , substr ast.Expr , iObj types.Object ) (negative , nonnegative , beforeSlice , afterSlice []ast.Expr ) {
390+ requireGuard := true
391+ if l := constSubstrLen (info , substr ); l != - 1 && l != 1 {
392+ requireGuard = false
393+ }
394+
378395 use := func (cur inspector.Cursor ) bool {
379396 ek := cur .ParentEdgeKind ()
380397 n := cur .Parent ().Node ()
381398 switch ek {
382399 case edge .BinaryExpr_X , edge .BinaryExpr_Y :
383400 check := n .(* ast.BinaryExpr )
384- switch checkIdxComparison (info , check ) {
401+ switch checkIdxComparison (info , check , iObj ) {
385402 case - 1 :
386403 negative = append (negative , check )
387404 return true
@@ -397,10 +414,10 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a
397414 if slice , ok := cur .Parent ().Parent ().Node ().(* ast.SliceExpr ); ok &&
398415 sameObject (info , s , slice .X ) &&
399416 slice .Max == nil {
400- if isBeforeSlice (info , ek , slice ) {
417+ if isBeforeSlice (info , ek , slice ) && ( ! requireGuard || isSliceIndexGuarded ( info , cur , iObj )) {
401418 beforeSlice = append (beforeSlice , slice )
402419 return true
403- } else if isAfterSlice (info , ek , slice , substr ) {
420+ } else if isAfterSlice (info , ek , slice , substr ) && ( ! requireGuard || isSliceIndexGuarded ( info , cur , iObj )) {
404421 afterSlice = append (afterSlice , slice )
405422 return true
406423 }
@@ -410,10 +427,10 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a
410427 // Check that the thing being sliced is s and that the slice doesn't
411428 // have a max index.
412429 if sameObject (info , s , slice .X ) && slice .Max == nil {
413- if isBeforeSlice (info , ek , slice ) {
430+ if isBeforeSlice (info , ek , slice ) && ( ! requireGuard || isSliceIndexGuarded ( info , cur , iObj )) {
414431 beforeSlice = append (beforeSlice , slice )
415432 return true
416- } else if isAfterSlice (info , ek , slice , substr ) {
433+ } else if isAfterSlice (info , ek , slice , substr ) && ( ! requireGuard || isSliceIndexGuarded ( info , cur , iObj )) {
417434 afterSlice = append (afterSlice , slice )
418435 return true
419436 }
@@ -465,8 +482,15 @@ func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPo
465482// Since strings.Index returns exactly -1 if the substring is not found, we
466483// don't need to handle expressions like i <= -3.
467484// We return 0 if the expression does not match any of these options.
468- // We assume that a check passed to checkIdxComparison has i as one of its operands.
469- func checkIdxComparison (info * types.Info , check * ast.BinaryExpr ) int {
485+ func checkIdxComparison (info * types.Info , check * ast.BinaryExpr , iObj types.Object ) int {
486+ isI := func (e ast.Expr ) bool {
487+ id , ok := e .(* ast.Ident )
488+ return ok && info .Uses [id ] == iObj
489+ }
490+ if ! isI (check .X ) && ! isI (check .Y ) {
491+ return 0
492+ }
493+
470494 // Ensure that the constant (if any) is on the right.
471495 x , op , y := check .X , check .Op , check .Y
472496 if info .Types [x ].Value != nil {
@@ -515,44 +539,49 @@ func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool {
515539 return ek == edge .SliceExpr_High && (slice .Low == nil || isZeroIntConst (info , slice .Low ))
516540}
517541
518- // isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):],
519- // or s[i + k:] where k is a const is equal to len(substr).
520- func isAfterSlice (info * types.Info , ek edge.Kind , slice * ast.SliceExpr , substr ast.Expr ) bool {
521- lowExpr , ok := slice .Low .(* ast.BinaryExpr )
522- if ! ok || slice .High != nil {
523- return false
524- }
525- // Returns true if the expression is a call to len(substr).
526- isLenCall := func (expr ast.Expr ) bool {
527- call , ok := expr .(* ast.CallExpr )
528- if ! ok || len (call .Args ) != 1 {
529- return false
530- }
531- return sameObject (info , substr , call .Args [0 ]) && typeutil .Callee (info , call ) == builtinLen
532- }
533-
542+ // constSubstrLen returns the constant length of substr, or -1 if unknown.
543+ func constSubstrLen (info * types.Info , substr ast.Expr ) int {
534544 // Handle len([]byte(substr))
535- if is [* ast.CallExpr ](substr ) {
536- call := substr .(* ast.CallExpr )
545+ if call , ok := substr .(* ast.CallExpr ); ok {
537546 tv := info .Types [call .Fun ]
538547 if tv .IsType () && types .Identical (tv .Type , byteSliceType ) {
539548 // Only one arg in []byte conversion.
540549 substr = call .Args [0 ]
541550 }
542551 }
543- substrLen := - 1
544552 substrVal := info .Types [substr ].Value
545553 if substrVal != nil {
546554 switch substrVal .Kind () {
547555 case constant .String :
548- substrLen = len (constant .StringVal (substrVal ))
556+ return len (constant .StringVal (substrVal ))
549557 case constant .Int :
550558 // constant.Value is a byte literal, e.g. bytes.IndexByte(_, 'a')
551559 // or a numeric byte literal, e.g. bytes.IndexByte(_, 65)
552- substrLen = 1
560+ // ([]byte(rune) is not legal.)
561+ return 1
562+ }
563+ }
564+ return - 1
565+ }
566+
567+ // isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):],
568+ // or s[i + k:] where k is a const is equal to len(substr).
569+ func isAfterSlice (info * types.Info , ek edge.Kind , slice * ast.SliceExpr , substr ast.Expr ) bool {
570+ lowExpr , ok := slice .Low .(* ast.BinaryExpr )
571+ if ! ok || slice .High != nil {
572+ return false
573+ }
574+ // Returns true if the expression is a call to len(substr).
575+ isLenCall := func (expr ast.Expr ) bool {
576+ call , ok := expr .(* ast.CallExpr )
577+ if ! ok || len (call .Args ) != 1 {
578+ return false
553579 }
580+ return sameObject (info , substr , call .Args [0 ]) && typeutil .Callee (info , call ) == builtinLen
554581 }
555582
583+ substrLen := constSubstrLen (info , substr )
584+
556585 switch ek {
557586 case edge .BinaryExpr_X :
558587 kVal := info .Types [lowExpr .Y ].Value
@@ -578,6 +607,75 @@ func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr a
578607 return false
579608}
580609
610+ // isSliceIndexGuarded reports whether a use of the index variable i (at the given cursor)
611+ // inside a slice expression is dominated by a nonnegative guard.
612+ // A use is considered guarded if any of the following are true:
613+ // - It is inside the Body of an IfStmt whose condition is a nonnegative check on i.
614+ // - It is inside the Else of an IfStmt whose condition is a negative check on i.
615+ // - It is preceded (in the same block) by an IfStmt whose condition is a
616+ // negative check on i with a terminating body (e.g., early return).
617+ //
618+ // Conversely, a use is immediately rejected if:
619+ // - It is inside the Body of an IfStmt whose condition is a negative check on i.
620+ // - It is inside the Else of an IfStmt whose condition is a nonnegative check on i.
621+ //
622+ // We have already checked (see [hasModifyingUses]) that there are no
623+ // intervening uses (incl. via aliases) of i that might alter its value.
624+ func isSliceIndexGuarded (info * types.Info , cur inspector.Cursor , iObj types.Object ) bool {
625+ for anc := range cur .Enclosing () {
626+ switch anc .ParentEdgeKind () {
627+ case edge .IfStmt_Body , edge .IfStmt_Else :
628+ ifStmt := anc .Parent ().Node ().(* ast.IfStmt )
629+ check := condChecksIdx (info , ifStmt .Cond , iObj )
630+ if anc .ParentEdgeKind () == edge .IfStmt_Else {
631+ check = - check
632+ }
633+ if check > 0 {
634+ return true // inside nonnegative-guarded block (i >= 0 here)
635+ }
636+ if check < 0 {
637+ return false // inside negative-guarded block (i < 0 here)
638+ }
639+ case edge .BlockStmt_List :
640+ // Check preceding siblings for early-return negative checks.
641+ for sib , ok := anc .PrevSibling (); ok ; sib , ok = sib .PrevSibling () {
642+ ifStmt , ok := sib .Node ().(* ast.IfStmt )
643+ if ok && condChecksIdx (info , ifStmt .Cond , iObj ) < 0 && bodyTerminates (ifStmt .Body ) {
644+ return true // preceded by early-return negative check
645+ }
646+ }
647+ case edge .FuncDecl_Body , edge .FuncLit_Body :
648+ return false // stop at function boundary
649+ }
650+ }
651+ return false
652+ }
653+
654+ // condChecksIdx reports whether cond is a BinaryExpr that checks
655+ // the index variable iObj for negativity or non-negativity.
656+ // Returns -1 for negative (e.g. i < 0), +1 for nonnegative (e.g. i >= 0), 0 otherwise.
657+ func condChecksIdx (info * types.Info , cond ast.Expr , iObj types.Object ) int {
658+ binExpr , ok := cond .(* ast.BinaryExpr )
659+ if ! ok {
660+ return 0
661+ }
662+ return checkIdxComparison (info , binExpr , iObj )
663+ }
664+
665+ // bodyTerminates reports whether the given block statement unconditionally
666+ // terminates execution (via return, break, continue, or goto).
667+ func bodyTerminates (block * ast.BlockStmt ) bool {
668+ if len (block .List ) == 0 {
669+ return false
670+ }
671+ last := block .List [len (block .List )- 1 ]
672+ switch last .(type ) {
673+ case * ast.ReturnStmt , * ast.BranchStmt :
674+ return true // return, break, continue, goto
675+ }
676+ return false
677+ }
678+
581679// sameObject reports whether we know that the expressions resolve to the same object.
582680func sameObject (info * types.Info , expr1 , expr2 ast.Expr ) bool {
583681 if ident1 , ok := expr1 .(* ast.Ident ); ok {
0 commit comments