diff --git a/go/analysis/passes/modernize/stringscut.go b/go/analysis/passes/modernize/stringscut.go
index f963b54..1e670d9 100644
--- a/go/analysis/passes/modernize/stringscut.go
+++ b/go/analysis/passes/modernize/stringscut.go
@@ -183,7 +183,7 @@
// len(substr)]), then we can replace the call to Index()
// with a call to Cut() and use the returned ok, before,
// and after variables accordingly.
- negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr)
+ negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr, iObj)
// Either there are no uses of before, after, or ok, or some use
// of i does not match our criteria - don't suggest a fix.
@@ -374,7 +374,15 @@
// 2. nonnegative - a condition equivalent to i >= 0
// 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i]
// 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))
-func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
+//
+// Additionally, all beforeSlice and afterSlice uses must be dominated by a
+// nonnegative guard on i (i.e., inside the body of an if whose condition
+// checks i >= 0, or in the else of a negative check, or after an
+// early-return negative check). This ensures that the rewrite from
+// s[i+len(sep):] to "after" preserves semantics, since when i == -1,
+// s[i+len(sep):] may yield a valid substring (e.g. s[0:] for single-byte
+// separators), but "after" would be "".
+func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr, iObj types.Object) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
use := func(cur inspector.Cursor) bool {
ek := cur.ParentEdgeKind()
n := cur.Parent().Node()
@@ -398,9 +406,15 @@
sameObject(info, s, slice.X) &&
slice.Max == nil {
if isBeforeSlice(info, ek, slice) {
+ if !isSliceGuarded(cur, info, iObj) {
+ return false
+ }
beforeSlice = append(beforeSlice, slice)
return true
} else if isAfterSlice(info, ek, slice, substr) {
+ if !isSliceGuarded(cur, info, iObj) {
+ return false
+ }
afterSlice = append(afterSlice, slice)
return true
}
@@ -411,9 +425,15 @@
// have a max index.
if sameObject(info, s, slice.X) && slice.Max == nil {
if isBeforeSlice(info, ek, slice) {
+ if !isSliceGuarded(cur, info, iObj) {
+ return false
+ }
beforeSlice = append(beforeSlice, slice)
return true
} else if isAfterSlice(info, ek, slice, substr) {
+ if !isSliceGuarded(cur, info, iObj) {
+ return false
+ }
afterSlice = append(afterSlice, slice)
return true
}
@@ -578,6 +598,92 @@
return false
}
+// isSliceGuarded reports whether a use of the index variable i (at the given cursor)
+// inside a slice expression is dominated by a nonnegative guard.
+// A use is considered guarded if any of the following are true:
+// - It is inside the Body of an IfStmt whose condition is a nonnegative check on i.
+// - It is inside the Else of an IfStmt whose condition is a negative check on i.
+// - It is preceded (in the same block) by an IfStmt whose condition is a
+// negative check on i with a terminating body (e.g., early return).
+//
+// Conversely, a use is immediately rejected if:
+// - It is inside the Body of an IfStmt whose condition is a negative check on i.
+// - It is inside the Else of an IfStmt whose condition is a nonnegative check on i.
+func isSliceGuarded(cur inspector.Cursor, info *types.Info, iObj types.Object) bool {
+ for anc := range cur.Enclosing() {
+ switch anc.ParentEdgeKind() {
+ case edge.IfStmt_Body:
+ ifStmt := anc.Parent().Node().(*ast.IfStmt)
+ check := condChecksIdx(info, ifStmt.Cond, iObj)
+ if check > 0 {
+ return true // inside nonnegative-guarded body
+ }
+ if check < 0 {
+ return false // inside negative-guarded body (i < 0 here)
+ }
+ case edge.IfStmt_Else:
+ ifStmt := anc.Parent().Node().(*ast.IfStmt)
+ check := condChecksIdx(info, ifStmt.Cond, iObj)
+ if check < 0 {
+ return true // inside else of negative-guarded if (i >= 0 here)
+ }
+ if check > 0 {
+ return false // inside else of nonnegative-guarded if (i < 0 here)
+ }
+ case edge.BlockStmt_List:
+ // Check preceding siblings for early-return negative checks.
+ for sib, ok := anc.PrevSibling(); ok; sib, ok = sib.PrevSibling() {
+ ifStmt, ok := sib.Node().(*ast.IfStmt)
+ if ok && condChecksIdx(info, ifStmt.Cond, iObj) < 0 && bodyTerminates(ifStmt.Body) {
+ return true // preceded by early-return negative check
+ }
+ }
+ case edge.FuncDecl_Body, edge.FuncLit_Body:
+ return false // stop at function boundary
+ }
+ }
+ return false
+}
+
+// condChecksIdx reports whether cond is a BinaryExpr that checks
+// the index variable iObj for negativity or non-negativity.
+// Returns -1 for negative (e.g. i < 0), +1 for nonnegative (e.g. i >= 0), 0 otherwise.
+func condChecksIdx(info *types.Info, cond ast.Expr, iObj types.Object) int {
+ binExpr, ok := cond.(*ast.BinaryExpr)
+ if !ok {
+ return 0
+ }
+ var refersToIdx bool
+ if ident, ok := binExpr.X.(*ast.Ident); ok {
+ refersToIdx = info.ObjectOf(ident) == iObj
+ }
+ if !refersToIdx {
+ if ident, ok := binExpr.Y.(*ast.Ident); ok {
+ refersToIdx = info.ObjectOf(ident) == iObj
+ }
+ }
+ if !refersToIdx {
+ return 0
+ }
+ return checkIdxComparison(info, binExpr)
+}
+
+// bodyTerminates reports whether the given block statement unconditionally
+// terminates execution (via return, break, continue, or goto).
+func bodyTerminates(block *ast.BlockStmt) bool {
+ if len(block.List) == 0 {
+ return false
+ }
+ last := block.List[len(block.List)-1]
+ switch last.(type) {
+ case *ast.ReturnStmt:
+ return true
+ case *ast.BranchStmt:
+ return true // break, continue, goto
+ }
+ return false
+}
+
// sameObject reports whether we know that the expressions resolve to the same object.
func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool {
if ident1, ok := expr1.(*ast.Ident); ok {
diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go
index 5990771..d91591a 100644
--- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go
+++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go
@@ -7,7 +7,7 @@
func basic(s string) bool {
s = "reassigned"
- i := strings.Index(s, "=") // want "strings.Index can be simplified using strings.Cut"
+ i := strings.Index(s, "=") // don't modernize: s[:i] is not guarded by i >= 0
print(s[:i])
return i >= 0
}
@@ -49,7 +49,7 @@
}
func basic_substr_arg(s string, substr string) bool {
- i := strings.Index(s, substr) // want "strings.Index can be simplified using strings.Cut"
+ i := strings.Index(s, substr) // don't modernize: s[i+len(substr):] is not guarded by i >= 0
print(s[i+len(substr):])
return i >= 0
}
@@ -61,26 +61,26 @@
}
func basic_strings_byte(s string) bool {
- i := strings.IndexByte(s, '+') // want "strings.IndexByte can be simplified using strings.Cut"
+ i := strings.IndexByte(s, '+') // don't modernize: s[:i] is not guarded by i >= 0
print(s[:i])
return i >= 0
}
func basic_strings_byte_int(s string) bool {
- i := strings.IndexByte(s, 55) // want "strings.IndexByte can be simplified using strings.Cut"
+ i := strings.IndexByte(s, 55) // don't modernize: s[:i] is not guarded by i >= 0
print(s[:i])
return i >= 0
}
func basic_strings_byte_var(s string) bool {
b := byte('b')
- i := strings.IndexByte(s, b) // want "strings.IndexByte can be simplified using strings.Cut"
+ i := strings.IndexByte(s, b) // don't modernize: s[:i] is not guarded by i >= 0
print(s[:i])
return i >= 0
}
func basic_bytes(b []byte) []byte {
- i := bytes.Index(b, []byte("str")) // want "bytes.Index can be simplified using bytes.Cut"
+ i := bytes.Index(b, []byte("str")) // don't modernize: b[i+3:] in else is not guarded
if i >= 0 {
return b[:i]
} else {
@@ -89,7 +89,7 @@
}
func basic_index_bytes(b []byte) string {
- i := bytes.IndexByte(b, 's') // want "bytes.IndexByte can be simplified using bytes.Cut"
+ i := bytes.IndexByte(b, 's') // don't modernize: b[i+1:] in else is not guarded
if i >= 0 {
return string(b[:i])
} else {
@@ -98,13 +98,13 @@
}
func const_substr_len(s string) bool {
- i := strings.Index(s, "=") // want "strings.Index can be simplified using strings.Cut"
+ i := strings.Index(s, "=") // don't modernize: s[i+len("="):] is not guarded
r := s[i+len("="):]
return len(r) > 0
}
func const_for_len(s string) bool {
- i := strings.Index(s, "=") // want "strings.Index can be simplified using strings.Cut"
+ i := strings.Index(s, "=") // don't modernize: s[i+1:] is not guarded
r := s[i+1:]
return len(r) > 0
}
@@ -154,7 +154,7 @@
func index_and_before_after(s string) string {
substr := "="
- i := strings.Index(s, substr) // want "strings.Index can be simplified using strings.Cut"
+ i := strings.Index(s, substr) // don't modernize: s[i+len(substr):] and s[len(substr)+i:] not nonneg-guarded
if i == -1 {
print("test")
}
@@ -174,7 +174,7 @@
}
func idx_var_init(s string) (string, string) {
- var idx = strings.Index(s, "=") // want "strings.Index can be simplified using strings.Cut"
+ var idx = strings.Index(s, "=") // don't modernize: s[0:idx] is not guarded
return s[0:idx], s
}
@@ -219,7 +219,7 @@
func s_in_func_call() string {
s := "string"
substr := "substr"
- idx := strings.Index(s, substr) // want "strings.Index can be simplified using strings.Cut"
+ idx := strings.Index(s, substr) // don't modernize: s[:idx] is not guarded
function(s)
return s[:idx]
}
@@ -306,3 +306,51 @@
func reference_int(i *int) {}
func blank() {}
+
+// Regression test for unguarded slice uses (https://go.dev/issue/77737).
+// The s[colon+1:] usage outside the "if" relies on -1+1=0 to return the full
+// string when the separator is absent. Rewriting to "after" would return "".
+func unguarded_after_slice(s string) (int, string) {
+ colon := strings.Index(s, ":")
+ if colon != -1 {
+ print(s[:colon])
+ }
+ return colon, s[colon+1:] // don't modernize: s[colon+1:] not guarded
+}
+
+// Same as above but with the guard using i < 0.
+func unguarded_after_slice_negcheck(s string) string {
+ i := strings.Index(s, ":")
+ if i < 0 {
+ print("not found")
+ }
+ return s[i+1:] // don't modernize: s[i+1:] not guarded
+}
+
+// Safe: both slice uses are inside the nonneg guard.
+func guarded_both_slices(s string) (string, string) {
+ i := strings.Index(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if i >= 0 {
+ return s[:i], s[i+1:]
+ }
+ return "", s
+}
+
+// Safe: slice uses after early-return negative check.
+func guarded_early_return(s string) (string, string) {
+ i := strings.Index(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if i < 0 {
+ return "", s
+ }
+ return s[:i], s[i+1:]
+}
+
+// Safe: slice uses in else of negative check.
+func guarded_neg_else(s string) (string, string) {
+ i := strings.Index(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if i == -1 {
+ return "", s
+ } else {
+ return s[:i], s[i+1:]
+ }
+}
diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden
index e274382..eb26502 100644
--- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden
+++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden
@@ -7,9 +7,9 @@
func basic(s string) bool {
s = "reassigned"
- before, _, ok := strings.Cut(s, "=") // want "strings.Index can be simplified using strings.Cut"
- print(before)
- return ok
+ i := strings.Index(s, "=") // don't modernize: s[:i] is not guarded by i >= 0
+ print(s[:i])
+ return i >= 0
}
func basic_contains(s string) bool {
@@ -49,9 +49,9 @@
}
func basic_substr_arg(s string, substr string) bool {
- _, after, ok := strings.Cut(s, substr) // want "strings.Index can be simplified using strings.Cut"
- print(after)
- return ok
+ i := strings.Index(s, substr) // don't modernize: s[i+len(substr):] is not guarded by i >= 0
+ print(s[i+len(substr):])
+ return i >= 0
}
func wrong_len_arg(s string, substr string) bool {
@@ -61,51 +61,51 @@
}
func basic_strings_byte(s string) bool {
- before, _, ok := strings.Cut(s, "+") // want "strings.IndexByte can be simplified using strings.Cut"
- print(before)
- return ok
+ i := strings.IndexByte(s, '+') // don't modernize: s[:i] is not guarded by i >= 0
+ print(s[:i])
+ return i >= 0
}
func basic_strings_byte_int(s string) bool {
- before, _, ok := strings.Cut(s, "7") // want "strings.IndexByte can be simplified using strings.Cut"
- print(before)
- return ok
+ i := strings.IndexByte(s, 55) // don't modernize: s[:i] is not guarded by i >= 0
+ print(s[:i])
+ return i >= 0
}
func basic_strings_byte_var(s string) bool {
b := byte('b')
- before, _, ok := strings.Cut(s, string(b)) // want "strings.IndexByte can be simplified using strings.Cut"
- print(before)
- return ok
+ i := strings.IndexByte(s, b) // don't modernize: s[:i] is not guarded by i >= 0
+ print(s[:i])
+ return i >= 0
}
func basic_bytes(b []byte) []byte {
- before, after, ok := bytes.Cut(b, []byte("str")) // want "bytes.Index can be simplified using bytes.Cut"
- if ok {
- return before
+ i := bytes.Index(b, []byte("str")) // don't modernize: b[i+3:] in else is not guarded
+ if i >= 0 {
+ return b[:i]
} else {
- return after
+ return b[i+3:]
}
}
func basic_index_bytes(b []byte) string {
- before, after, ok := bytes.Cut(b, []byte{'s'}) // want "bytes.IndexByte can be simplified using bytes.Cut"
- if ok {
- return string(before)
+ i := bytes.IndexByte(b, 's') // don't modernize: b[i+1:] in else is not guarded
+ if i >= 0 {
+ return string(b[:i])
} else {
- return string(after)
+ return string(b[i+1:])
}
}
func const_substr_len(s string) bool {
- _, after, _ := strings.Cut(s, "=") // want "strings.Index can be simplified using strings.Cut"
- r := after
+ i := strings.Index(s, "=") // don't modernize: s[i+len("="):] is not guarded
+ r := s[i+len("="):]
return len(r) > 0
}
func const_for_len(s string) bool {
- _, after, _ := strings.Cut(s, "=") // want "strings.Index can be simplified using strings.Cut"
- r := after
+ i := strings.Index(s, "=") // don't modernize: s[i+1:] is not guarded
+ r := s[i+1:]
return len(r) > 0
}
@@ -154,28 +154,28 @@
func index_and_before_after(s string) string {
substr := "="
- before, after, ok := strings.Cut(s, substr) // want "strings.Index can be simplified using strings.Cut"
- if !ok {
+ i := strings.Index(s, substr) // don't modernize: s[i+len(substr):] and s[len(substr)+i:] not nonneg-guarded
+ if i == -1 {
print("test")
}
- if !ok {
+ if i < 0 {
return ""
} else {
- if ok {
- return before
+ if i >= 0 {
+ return s[:i]
} else {
- return after
+ return s[i+len(substr):]
}
}
- if !ok {
- return after
+ if -1 == i {
+ return s[len(substr)+i:]
}
return "final"
}
func idx_var_init(s string) (string, string) {
- var before, _, _ = strings.Cut(s, "=") // want "strings.Index can be simplified using strings.Cut"
- return before, s
+ var idx = strings.Index(s, "=") // don't modernize: s[0:idx] is not guarded
+ return s[0:idx], s
}
func idx_reassigned(s string) string {
@@ -219,9 +219,9 @@
func s_in_func_call() string {
s := "string"
substr := "substr"
- before, _, _ := strings.Cut(s, substr) // want "strings.Index can be simplified using strings.Cut"
+ idx := strings.Index(s, substr) // don't modernize: s[:idx] is not guarded
function(s)
- return before
+ return s[:idx]
}
func s_pointer() string {
@@ -305,4 +305,52 @@
func reference_int(i *int) {}
-func blank() {}
\ No newline at end of file
+func blank() {}
+
+// Regression test for unguarded slice uses (https://go.dev/issue/77737).
+// The s[colon+1:] usage outside the "if" relies on -1+1=0 to return the full
+// string when the separator is absent. Rewriting to "after" would return "".
+func unguarded_after_slice(s string) (int, string) {
+ colon := strings.Index(s, ":")
+ if colon != -1 {
+ print(s[:colon])
+ }
+ return colon, s[colon+1:] // don't modernize: s[colon+1:] not guarded
+}
+
+// Same as above but with the guard using i < 0.
+func unguarded_after_slice_negcheck(s string) string {
+ i := strings.Index(s, ":")
+ if i < 0 {
+ print("not found")
+ }
+ return s[i+1:] // don't modernize: s[i+1:] not guarded
+}
+
+// Safe: both slice uses are inside the nonneg guard.
+func guarded_both_slices(s string) (string, string) {
+ before, after, ok := strings.Cut(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if ok {
+ return before, after
+ }
+ return "", s
+}
+
+// Safe: slice uses after early-return negative check.
+func guarded_early_return(s string) (string, string) {
+ before, after, ok := strings.Cut(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if !ok {
+ return "", s
+ }
+ return before, after
+}
+
+// Safe: slice uses in else of negative check.
+func guarded_neg_else(s string) (string, string) {
+ before, after, ok := strings.Cut(s, ":") // want "strings.Index can be simplified using strings.Cut"
+ if !ok {
+ return "", s
+ } else {
+ return before, after
+ }
+}
diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go
index a50b5d9..d97b914 100644
--- a/gopls/internal/test/integration/completion/completion_test.go
+++ b/gopls/internal/test/integration/completion/completion_test.go
@@ -706,20 +706,17 @@
}
for _, tc := range testcases {
- t.Run(fmt.Sprintf("%v/%v", tc.mode, tc.accept), func(t *testing.T) {
-
- env.SetSuggestionInsertReplaceMode(tc.mode == "replace")
- env.SetBufferContent("main.go", orig)
- loc := env.RegexpSearch("main.go", `()Lower\)`)
- completions := env.Completion(loc)
- item := find(t, completions, tc.accept)
- env.AcceptCompletion(loc, item)
- env.Await(env.DoneWithChange())
- got := env.BufferText("main.go")
- if !strings.Contains(got, tc.want) {
- t.Errorf("unexpected state after completion:\n%v\nwanted %v", got, tc.want)
- }
- })
+ env.SetSuggestionInsertReplaceMode(tc.mode == "replace")
+ env.SetBufferContent("main.go", orig)
+ loc := env.RegexpSearch("main.go", `()Lower\)`)
+ completions := env.Completion(loc)
+ item := find(t, completions, tc.accept)
+ env.AcceptCompletion(loc, item)
+ env.Await(env.DoneWithChange())
+ got := env.BufferText("main.go")
+ if !strings.Contains(got, tc.want) {
+ t.Errorf("%v/%v: unexpected state after completion:\n%v\nwanted %v", tc.mode, tc.accept, got, tc.want)
+ }
}
})
})