[tools] gopls/internal/analysis/modernize: add strings.Cut modernizer

9 views
Skip to first unread message

Madeline Kalil (Gerrit)

unread,
Sep 25, 2025, 3:44:59 PMSep 25
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Madeline Kalil has uploaded the change for review

Commit message

gopls/internal/analysis/modernize: add strings.Cut modernizer

This modernizer offers to replace instances of strings.Index,
strings.IndexByte, bytes.Index, and bytes.IndexByte, with either
strings.Cut or bytes.Cut. It only suggests a fix when the expressions
involving the returned index and the passed string and substring params
meet certain criteria.

Fixes golang/go#71369
Change-Id: Iff6c113587ba31664578baf1e757929d749363cb

Change diff

diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 16a5875..6fa71d6 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -3954,6 +3954,33 @@

Package documentation: [stringsbuilder](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringbuilder)

+<a id='stringscut'></a>
+## `stringscut`: replace strings.Index etc. with strings.Cut
+
+
+This analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.
+
+For example:
+
+ idx := strings.Index(s, substr)
+ if i >= 0 {
+ return s[:idx]
+ }
+
+is replaced by:
+
+ before, _, ok := strings.Cut(s, substr)
+ if ok {
+ return before
+ }
+
+This analyzer requires that all uses of idx and s match forms
+that can be resolved by replacements with the before, after, and ok variables returned by strings.Cut. It also requires that are no modifications or potential modifications to idx, s, and substr.
+
+Default: on.
+
+Package documentation: [stringscut](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut)
+
<a id='stringscutprefix'></a>
## `stringscutprefix`: replace HasPrefix/TrimPrefix with CutPrefix

diff --git a/gopls/internal/analysis/modernize/doc.go b/gopls/internal/analysis/modernize/doc.go
index 376c537..f9c314e 100644
--- a/gopls/internal/analysis/modernize/doc.go
+++ b/gopls/internal/analysis/modernize/doc.go
@@ -244,6 +244,29 @@

with the simpler `slices.Sort(s)`, which was added in Go 1.21.

+# Analyzer stringscut
+
+stringscut: replace strings.Index etc. with strings.Cut
+
+This analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.
+
+For example:
+
+ idx := strings.Index(s, substr)
+ if i >= 0 {
+ return s[:idx]
+ }
+
+is replaced by:
+
+ before, _, ok := strings.Cut(s, substr)
+ if ok {
+ return before
+ }
+
+This analyzer requires that all uses of idx and s match forms
+that can be resolved by replacements with the before, after, and ok variables returned by strings.Cut. It also requires that are no modifications or potential modifications to idx, s, and substr.
+
# Analyzer stringscutprefix

stringscutprefix: replace HasPrefix/TrimPrefix with CutPrefix
diff --git a/gopls/internal/analysis/modernize/modernize.go b/gopls/internal/analysis/modernize/modernize.go
index 08cb58f..625bd07 100644
--- a/gopls/internal/analysis/modernize/modernize.go
+++ b/gopls/internal/analysis/modernize/modernize.go
@@ -45,6 +45,7 @@
// SlicesDeleteAnalyzer, // not nil-preserving!
SlicesSortAnalyzer,
StringsCutPrefixAnalyzer,
+ StringsCutAnalyzer,
StringsSeqAnalyzer,
StringsBuilderAnalyzer,
TestingContextAnalyzer,
diff --git a/gopls/internal/analysis/modernize/modernize_test.go b/gopls/internal/analysis/modernize/modernize_test.go
index aafaab8..0be8c3d 100644
--- a/gopls/internal/analysis/modernize/modernize_test.go
+++ b/gopls/internal/analysis/modernize/modernize_test.go
@@ -70,6 +70,10 @@
RunWithSuggestedFixes(t, TestData(), modernize.StringsBuilderAnalyzer, "stringsbuilder")
}

+func TestStringsCut(t *testing.T) {
+ RunWithSuggestedFixes(t, TestData(), modernize.StringsCutAnalyzer, "stringscut")
+}
+
func TestStringsCutPrefix(t *testing.T) {
RunWithSuggestedFixes(t, TestData(), modernize.StringsCutPrefixAnalyzer,
"stringscutprefix",
diff --git a/gopls/internal/analysis/modernize/stringscut.go b/gopls/internal/analysis/modernize/stringscut.go
new file mode 100644
index 0000000..baeb280
--- /dev/null
+++ b/gopls/internal/analysis/modernize/stringscut.go
@@ -0,0 +1,453 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package modernize
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/format"
+ "go/token"
+ "go/types"
+ "iter"
+ "slices"
+ "strconv"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/analysisinternal/generated"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+var StringsCutAnalyzer = &analysis.Analyzer{
+ Name: "stringscut",
+ Doc: analysisinternal.MustExtractDoc(doc, "stringscut"),
+ Requires: []*analysis.Analyzer{
+ generated.Analyzer,
+ inspect.Analyzer,
+ typeindexanalyzer.Analyzer,
+ },
+ Run: stringscut,
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut",
+}
+
+// stringscut offers a fix to replace an occurence of strings.Index, strings.IndexByte, bytes.Index,
+// or bytes.IndexByte with strings.Cut or bytes.Cut.
+// Consider some candidate for replacement idx := strings.Index(s, substr). The following must hold for a replacement to occur:
+//
+// 1. All instances of idx and s must be in one of these forms.
+// Binary expressions:
+// (a): idx < 0, 0 > idx, idx == -1, -1 == idx
+// (b): idx >= 0, 0 <= idx
+//
+// Slice expressions:
+// a: s[:idx], s[0:idx]
+// b: s[idx+len(substr):], s[len(substr) + idx:], s[idx + const], s[const + idx] (where const = len(substr))
+//
+// 2. There can be no uses of s, substr, or idx where they are
+// potentially modified (i.e. in assignments, or function calls with unknown side
+// effects).
+//
+// Then, the replacement involves the following substitutions:
+// 1. Replace "idx := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)"
+// 2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok.
+// 3. Replace slice expressions (a) with "before" and slice expressions (b) with after.
+// 4. The assignments to before, after, and ok may use the blank identifier "_" if they are unused.
+
+func stringscut(pass *analysis.Pass) (any, error) {
+ skipGenerated(pass)
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+
+ stringsIndex = index.Object("strings", "Index")
+ stringsIndexByte = index.Object("strings", "IndexByte")
+ bytesIndex = index.Object("bytes", "Index")
+ bytesIndexByte = index.Object("bytes", "IndexByte")
+ )
+ if !index.Used(stringsIndex, stringsIndexByte, bytesIndex, bytesIndexByte) {
+ return nil, nil
+ }
+
+ for curFile := range filesUsing(inspect, info, "go1.18") {
+ for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
+ indexCall := curCall.Node().(*ast.CallExpr)
+ obj := typeutil.Callee(info, indexCall)
+ if obj == nil {
+ continue
+ }
+ var (
+ isStringsIndex = analysisinternal.IsFunctionNamed(obj, "strings", "Index")
+ isBytesIndex = analysisinternal.IsFunctionNamed(obj, "bytes", "Index")
+ isStringsIndexByte = analysisinternal.IsFunctionNamed(obj, "strings", "IndexByte")
+ isBytesIndexByte = analysisinternal.IsFunctionNamed(obj, "bytes", "IndexByte")
+ )
+ if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
+ continue
+ }
+ // Check if assigning idx with := or =, since the method to extract
+ // the *ast.Ident corresponding to idx will differ depending on
+ // which assignment is used.
+ callParent := curCall.Parent()
+ assign, okAsgn := callParent.Node().(*ast.AssignStmt) // :=
+ decl, okDecl := callParent.Parent().Node().(*ast.GenDecl) // var _ =
+ if okAsgn || okDecl {
+ var (
+ idx *ast.Ident
+ ok bool
+ )
+ if okAsgn {
+ // TODO(mkalil): Handle multiple assign statements.
+ if len(assign.Lhs) != 1 {
+ continue
+ }
+ lhs := assign.Lhs[0]
+ idx, ok = lhs.(*ast.Ident)
+ if !ok {
+ continue
+ }
+ } else if okDecl {
+ // TODO(mkalil): Handle var declarations with multiple specs.
+ if len(decl.Specs) != 1 || decl.Tok != token.VAR {
+ continue
+ }
+ idx = decl.Specs[0].(*ast.ValueSpec).Names[0]
+ }
+
+ // Have idx := strings.Index(s, substr) or var idx = strings.Index(s, substr)
+ // (or bytes.Index, etc.)
+ //
+ // Next, examine all uses of idx. If the only uses are of the
+ // forms mentioned above (i.e. idx < 0, idx >= 0, s[:idx] and s[idx +
+ // len(substr)]), then we can replace the call to strings.Index
+ // with a call to strings.Cut and use the returned ok, before,
+ // and after variables accordingly.
+
+ // Get uses of idx.
+ idxObj := info.Defs[idx]
+ idxUses := index.Uses(idxObj)
+
+ s := indexCall.Args[0]
+ substr := indexCall.Args[1]
+ lessZero, greaterZero, beforeSlice, afterSlice := parseIdxUses(pass, idxUses, idx, s, substr, indexCall.Pos())
+
+ // Either there are no uses of before, after, or ok, or some use
+ // of idx does not match our criteria - don't suggest a fix.
+ if lessZero == nil && greaterZero == nil && beforeSlice == nil && afterSlice == nil {
+ continue
+ }
+
+ // Check that there are no statements that alter the value of s
+ // or substr after the call to Index(). s and substr could be
+ // idents or string/byte literals. If they are the latter, we don't
+ // need to check their uses since they cannot be modified.
+ afterPos := indexCall.Pos() // don't consider uses before the call to strings.Index etc.
+ ids := []ast.Expr{s, substr}
+ modified := false
+ for _, id := range ids {
+ if sId, ok := id.(*ast.Ident); ok {
+ sObj := info.Uses[sId]
+ sUses := index.Uses(sObj)
+ modified = hasModifyingUses(sUses, afterPos)
+ if modified {
+ break
+ }
+ }
+ }
+ if modified {
+ continue
+ }
+
+ scope := idxObj.Parent()
+ okVarName := analysisinternal.FreshName(scope, idx.Pos(), "ok")
+ beforeVarName := analysisinternal.FreshName(scope, idx.Pos(), "before")
+ afterVarName := analysisinternal.FreshName(scope, idx.Pos(), "after")
+
+ var textEdits []analysis.TextEdit
+ changes := [][]ast.Expr{lessZero, greaterZero, beforeSlice, afterSlice}
+ newText := []string{"!" + okVarName, okVarName, beforeVarName, afterVarName}
+ for i, exprs := range changes {
+ for _, expr := range exprs {
+ textEdits = append(textEdits, analysis.TextEdit{
+ Pos: expr.Pos(),
+ End: expr.End(),
+ NewText: []byte(newText[i]),
+ })
+ }
+ }
+
+ // Format s and substr.
+ var bufS bytes.Buffer
+ err := format.Node(&bufS, pass.Fset, s)
+ if err != nil {
+ return nil, err
+ }
+ var bufSubstr bytes.Buffer
+ err = format.Node(&bufSubstr, pass.Fset, substr)
+ if err != nil {
+ return nil, err
+ }
+
+ // If there will be no uses of ok, before, or after, use the
+ // blank identifier instead.
+ if len(lessZero) == 0 && len(greaterZero) == 0 {
+ okVarName = "_"
+ }
+ if len(beforeSlice) == 0 {
+ beforeVarName = "_"
+ }
+ if len(afterSlice) == 0 {
+ afterVarName = "_"
+ }
+
+ var bytesOrStrings string
+ if isBytesIndex || isBytesIndexByte {
+ bytesOrStrings = "bytes"
+ } else if isStringsIndex || isStringsIndexByte {
+ bytesOrStrings = "strings"
+ }
+
+ // Replace the function call depending on whether we have an assignment or var declaration.
+ var pos, end token.Pos
+ if okAsgn {
+ pos, end = assign.Pos(), assign.End()
+ textEdits = append(textEdits, analysis.TextEdit{
+ Pos: pos,
+ End: end,
+ NewText: fmt.Appendf(nil, "%s, %s, %s := %s.Cut(%s, %s)", beforeVarName, afterVarName, okVarName, bytesOrStrings, bufS.String(), bufSubstr.String()),
+ })
+ } else if okDecl {
+ pos, end = decl.Pos(), decl.End()
+ textEdits = append(textEdits, analysis.TextEdit{
+ Pos: pos,
+ End: end,
+ NewText: fmt.Appendf(nil, "var %s, %s, %s = %s.Cut(%s, %s)", beforeVarName, afterVarName, okVarName, bytesOrStrings, bufS.String(), bufSubstr.String()),
+ })
+ }
+
+ var replace, with string
+ if isBytesIndex {
+ replace, with = "bytes.Index", "bytes.Cut"
+ } else if isStringsIndex {
+ replace, with = "strings.Index", "strings.Cut"
+ } else if isBytesIndexByte {
+ replace, with = "bytes.IndexByte", "bytes.Cut"
+ } else if isStringsIndexByte {
+ replace, with = "strings.IndexByte", "strings.Cut"
+ }
+ pass.Report(analysis.Diagnostic{
+ Pos: pos,
+ End: end,
+ Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
+ Category: "stringscut",
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
+ TextEdits: textEdits,
+ }},
+ })
+ }
+ }
+ }
+
+ return nil, nil
+}
+
+// If all uses of idx match one of the valid formats, return a list of occurences for each format.
+// If any of the uses do not match one of the formats, return nil for all values.
+// Only examine uses after the given token.Pos.
+func parseIdxUses(pass *analysis.Pass, uses iter.Seq[inspector.Cursor], idx *ast.Ident, s, substr ast.Expr, afterPos token.Pos) (lessZero, greaterZero, beforeSlice, afterSlice []ast.Expr) {
+ for cur := range uses {
+ for c := range cur.Enclosing((*ast.BinaryExpr)(nil), (*ast.SliceExpr)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil), (*ast.GenDecl)(nil)) {
+ n := c.Node()
+ if n.Pos() <= afterPos {
+ continue
+ }
+ switch n := n.(type) {
+ case *ast.BinaryExpr:
+ check := n
+ if isLessThanZeroCheck(check) {
+ lessZero = append(lessZero, check)
+ } else if isGreaterEqualToZeroCheck(check) {
+ greaterZero = append(greaterZero, check)
+ } else {
+ // Ok if part of SliceExpr, those will be checked elsewhere
+ if _, ok := c.Parent().Node().(*ast.SliceExpr); ok {
+ continue
+ }
+ return nil, nil, nil, nil
+ }
+ case *ast.SliceExpr:
+ slice := n
+ if !equalSyntax(slice.X, s) {
+ continue
+ }
+ if isBeforeSlice(slice, idx) {
+ beforeSlice = append(beforeSlice, slice)
+ } else if isAfterSlice(pass, slice, idx, substr) {
+ afterSlice = append(afterSlice, slice)
+ } else {
+ return nil, nil, nil, nil
+ }
+ case *ast.AssignStmt:
+ // If the value of idx is reassigned, don't suggest a fix.
+ assign := n
+ for _, expr := range assign.Lhs {
+ if equalSyntax(expr, idx) {
+ return nil, nil, nil, nil
+ }
+ }
+ // If an alias to idx is created, don't suggest a fix.
+ for _, expr := range assign.Rhs {
+ if equalSyntax(expr, idx) {
+ return nil, nil, nil, nil
+ }
+ }
+ case *ast.GenDecl:
+ // If an alias to idx is created, don't suggest a fix.
+ decl := n
+ for i, spec := range decl.Specs {
+ if valueSpec, ok := spec.(*ast.ValueSpec); ok {
+ if equalSyntax(valueSpec.Values[i], idx) {
+ return nil, nil, nil, nil
+ }
+ }
+ }
+ case *ast.CallExpr:
+ // Don't suggest a fix if idx is passed as an argument to some
+ // other function (unless it's the call to strings.Index).
+ call := n
+ for _, arg := range call.Args {
+ if equalSyntax(arg, idx) {
+ return nil, nil, nil, nil
+ }
+ }
+ }
+
+ }
+
+ }
+
+ return lessZero, greaterZero, beforeSlice, afterSlice
+}
+
+// Returns true if any of the uses involve modifying the ident like in an
+// assignment statement, or potential unknown side effects, like via passing the
+// ident to a function. Any use before the "afterPos" won't be considered.
+func hasModifyingUses(uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
+ for use := range uses {
+ for cur := range use.Enclosing((*ast.CallExpr)(nil), (*ast.AssignStmt)(nil)) {
+ if cur.Node().Pos() <= afterPos {
+ continue
+ }
+ switch n := cur.Node().(type) {
+ case *ast.CallExpr:
+ // Allow len(substr) and string() conversion, but any other
+ // CallExpr involving the ident will result in not returning a
+ // quick fix.
+ if isCall(n, "len", "string") {
+ continue
+ } else {
+ return true
+ }
+ case *ast.AssignStmt:
+ if equalSyntax(n.Lhs[0], use.Node().(*ast.Ident)) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+// Returns true if the BinaryExpr is of the form idx < 0, 0 > idx, idx == -1, or
+// -1 == idx.
+func isLessThanZeroCheck(check *ast.BinaryExpr) bool {
+ return check.Op == token.LSS && typesinternal.IsZeroExpr(check.Y) || check.Op == token.GTR && typesinternal.IsZeroExpr(check.X) || (check.Op == token.EQL && (isNegativeOne(check.X) || isNegativeOne(check.Y)))
+}
+
+// Returns true if the expr is equivalent to -1.
+func isNegativeOne(expr ast.Expr) bool {
+ switch e := expr.(type) {
+ case *ast.UnaryExpr:
+ x, ok := e.X.(*ast.BasicLit)
+ return ok && e.Op == token.SUB && x.Value == "1"
+ default:
+ return false
+ }
+}
+
+// Returns true if the BinaryExpr is of the form idx >= 0 or 0 <= idx.
+func isGreaterEqualToZeroCheck(check *ast.BinaryExpr) bool {
+ return check.Op == token.GEQ && typesinternal.IsZeroExpr(check.Y) || check.Op == token.LEQ && typesinternal.IsZeroExpr(check.X)
+}
+
+// Returns true if the SliceExpr is of the form s[:idx] or s[0:idx].
+func isBeforeSlice(slice *ast.SliceExpr, idx ast.Expr) bool {
+ return equalSyntax(slice.High, idx) && (slice.Low == nil || typesinternal.IsZeroExpr(slice.Low))
+}
+
+// Returns true if the SliceExpr is of the form s[idx+len(substr):],
+// s[len(substr) + idx:], or s[idx + const] where const is equal to len(substr).
+func isAfterSlice(pass *analysis.Pass, slice *ast.SliceExpr, idx, substr ast.Expr) bool {
+ expr, ok := slice.Low.(*ast.BinaryExpr)
+ if !ok {
+ return false
+ }
+ // Handle s[idx + const] where substr is a basic lit
+ // substr could also be a call expr like []byte("str")
+ // TODO(mkalil): This does not handle other methods used to create byte slices
+ // like byte literals, or using make/append.
+ var substrLit *ast.BasicLit
+ switch n := substr.(type) {
+ case *ast.BasicLit:
+ substrLit = n
+ case *ast.CallExpr:
+ // Check if call matches []byte
+ tv := pass.TypesInfo.Types[n.Fun]
+ if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
+ // Only one arg in []byte conversion.
+ if lit, ok := n.Args[0].(*ast.BasicLit); ok {
+ substrLit = lit
+ }
+ }
+ }
+
+ if substrLit != nil {
+ substr_len := len(substrLit.Value) - 2 //BasicLit includes beginning and ending quote
+ if lit, ok := expr.X.(*ast.BasicLit); ok && equalSyntax(expr.Y, idx) {
+ num, err := strconv.Atoi(lit.Value)
+ if err != nil {
+ return false
+ }
+ return num == substr_len
+ }
+ if lit, ok := expr.Y.(*ast.BasicLit); ok && equalSyntax(expr.X, idx) {
+ num, err := strconv.Atoi(lit.Value)
+ if err != nil {
+ return false
+ }
+ return num == substr_len
+ }
+ }
+
+ return ok && slice.High == nil && expr.Op == token.ADD &&
+ (equalSyntax(expr.X, idx) && isCall(expr.Y, "len") || equalSyntax(expr.Y, idx) && isCall(expr.X, "len"))
+}
+
+// Returns true if the call expression matches the form callName(substr) for any of the names provided.
+func isCall(expr ast.Expr, callName ...string) bool {
+ call, ok := expr.(*ast.CallExpr)
+ if !ok {
+ return false
+ }
+ ident, ok := call.Fun.(*ast.Ident)
+ return ok && len(call.Args) == 1 && slices.Contains(callName, ident.Name)
+}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go b/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go
new file mode 100644
index 0000000..1c1dc69
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go
@@ -0,0 +1,162 @@
+package stringscut
+
+import (
+ "bytes"
+ "strings"
+)
+
+func basic(s string) bool {
+ s = "reassigned"
+ i := strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ return i >= 0
+}
+
+func basic_strings_byte(s string) bool {
+ i := strings.IndexByte(s, '+') // want "strings.IndexByte can be replaced with strings.Cut"
+ return i >= 0
+}
+
+func basic_bytes(b []byte) []byte {
+ i := bytes.Index(b, []byte("str")) // want "bytes.Index can be replaced with bytes.Cut"
+ if i >= 0 {
+ return b[:i]
+ } else {
+ return b[i+3:]
+ }
+}
+
+func basic_index_bytes(b []byte) string {
+ i := bytes.IndexByte(b, 's') // want "bytes.IndexByte can be replaced with bytes.Cut"
+ if i >= 0 {
+ return string(b[:i])
+ } else {
+ return string(b[i+1:])
+ }
+}
+
+func const_substr_len(s string) bool {
+ i := strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ r := s[i+len("="):]
+ return len(r) > 0
+}
+
+func const_for_len(s string) bool {
+ i := strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ r := s[i+1:]
+ return len(r) > 0
+}
+
+func index(s string) bool {
+ i := strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ if i < 0 {
+ return false
+ }
+ if i >= 0 {
+ return true
+ }
+ return true
+}
+
+func index_flipped(s string) bool {
+ i := strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ if 0 > i {
+ return false
+ }
+ if 0 <= i {
+ return true
+ }
+ return true
+}
+
+func invalid_index(s string) bool {
+ i := strings.Index(s, "=") // don't modernize since i is used in an "invalid" binaryexpr
+ if 0 > i {
+ return false
+ }
+ if i < 10 {
+ return true
+ }
+ return true
+}
+
+func invalid_slice(s string) string {
+ i := strings.Index(s, "=") // don't modernize since i is used in an "invalid" slice index
+ if i >= 0 {
+ return s[i+4:]
+ }
+ return ""
+}
+
+func index_and_before_after(s string) string {
+ substr := "="
+ i := strings.Index(s, substr) // want "strings.Index can be replaced with strings.Cut"
+ if i == -1 {
+ print("test")
+ }
+ if i < 0 {
+ return ""
+ } else {
+ if i >= 0 {
+ return s[:i]
+ } else {
+ return s[i+len(substr):]
+ }
+ }
+ if -1 == i {
+ return s[len(substr)+i:]
+ }
+ return "final"
+}
+
+func idx_var_init(s string) (string, string) {
+ var idx = strings.Index(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ return s[0:idx], s
+}
+
+func idx_reassigned(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets reassigned
+ idx = 10
+ return s[:idx]
+}
+
+func idx_printed(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx is used
+ print(idx)
+ return s[:idx]
+}
+
+func idx_aliased(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets aliased
+ i := idx
+ return s[:i]
+}
+
+func idx_aliased_var(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets aliased
+ var i = idx
+ print(i)
+ return s[:idx]
+}
+
+func s_modified(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since s gets modified
+ s = "newstring"
+ return s[:idx]
+}
+
+func s_modified_no_params() string {
+ s := "string"
+ idx := strings.Index(s, "=") // don't modernize since s gets modified
+ s = "newstring"
+ return s[:idx]
+}
+
+func s_in_func_call() string {
+ s := "string"
+ substr := "substr"
+ idx := strings.Index(s, substr) // don't modernize since s may get modified
+ some_func_that_may_change_s(s)
+ return s[:idx]
+}
+
+func some_func_that_may_change_s(s string) {}
diff --git a/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go.golden b/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go.golden
new file mode 100644
index 0000000..297e6b0
--- /dev/null
+++ b/gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go.golden
@@ -0,0 +1,162 @@
+package stringscut
+
+import (
+ "bytes"
+ "strings"
+)
+
+func basic(s string) bool {
+ s = "reassigned"
+ _, _, ok := strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ return ok
+}
+
+func basic_strings_byte(s string) bool {
+ _, _, ok := strings.Cut(s, '+') // want "strings.IndexByte can be replaced with strings.Cut"
+ return ok
+}
+
+func basic_bytes(b []byte) []byte {
+ before, after, ok := bytes.Cut(b, []byte("str")) // want "bytes.Index can be replaced with bytes.Cut"
+ if ok {
+ return before
+ } else {
+ return after
+ }
+}
+
+func basic_index_bytes(b []byte) string {
+ before, after, ok := bytes.Cut(b, 's') // want "bytes.IndexByte can be replaced with bytes.Cut"
+ if ok {
+ return string(before)
+ } else {
+ return string(after)
+ }
+}
+
+func const_substr_len(s string) bool {
+ _, after, _ := strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ r := after
+ return len(r) > 0
+}
+
+func const_for_len(s string) bool {
+ _, after, _ := strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ r := after
+ return len(r) > 0
+}
+
+func index(s string) bool {
+ _, _, ok := strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ if !ok {
+ return false
+ }
+ if ok {
+ return true
+ }
+ return true
+}
+
+func index_flipped(s string) bool {
+ _, _, ok := strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ if !ok {
+ return false
+ }
+ if ok {
+ return true
+ }
+ return true
+}
+
+func invalid_index(s string) bool {
+ i := strings.Index(s, "=") // don't modernize since i is used in an "invalid" binaryexpr
+ if 0 > i {
+ return false
+ }
+ if i < 10 {
+ return true
+ }
+ return true
+}
+
+func invalid_slice(s string) string {
+ i := strings.Index(s, "=") // don't modernize since i is used in an "invalid" slice index
+ if i >= 0 {
+ return s[i+4:]
+ }
+ return ""
+}
+
+func index_and_before_after(s string) string {
+ substr := "="
+ before, after, ok := strings.Cut(s, substr) // want "strings.Index can be replaced with strings.Cut"
+ if !ok {
+ print("test")
+ }
+ if !ok {
+ return ""
+ } else {
+ if ok {
+ return before
+ } else {
+ return after
+ }
+ }
+ if !ok {
+ return after
+ }
+ return "final"
+}
+
+func idx_var_init(s string) (string, string) {
+ var before, _, _ = strings.Cut(s, "=") // want "strings.Index can be replaced with strings.Cut"
+ return before, s
+}
+
+func idx_reassigned(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets reassigned
+ idx = 10
+ return s[:idx]
+}
+
+func idx_printed(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx is used
+ print(idx)
+ return s[:idx]
+}
+
+func idx_aliased(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets aliased
+ i := idx
+ return s[:i]
+}
+
+func idx_aliased_var(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since idx gets aliased
+ var i = idx
+ print(i)
+ return s[:idx]
+}
+
+func s_modified(s string) string {
+ idx := strings.Index(s, "=") // don't modernize since s gets modified
+ s = "newstring"
+ return s[:idx]
+}
+
+func s_modified_no_params() string {
+ s := "string"
+ idx := strings.Index(s, "=") // don't modernize since s gets modified
+ s = "newstring"
+ return s[:idx]
+}
+
+func s_in_func_call() string {
+ s := "string"
+ substr := "substr"
+ idx := strings.Index(s, substr) // don't modernize since s may get modified
+ some_func_that_may_change_s(s)
+ return s[:idx]
+}
+
+func some_func_that_may_change_s(s string) {}
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index 08b11d3..e9698d9 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -1683,6 +1683,12 @@
"Status": ""
},
{
+ "Name": "\"stringscut\"",
+ "Doc": "replace strings.Index etc. with strings.Cut\n\nThis analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.\n\nFor example:\n\n\tidx := strings.Index(s, substr)\n\tif i \u003e= 0 {\n\t return s[:idx]\n\t}\n\nis replaced by:\n\n\tbefore, _, ok := strings.Cut(s, substr)\n\tif ok {\n\t return before\n\t}\n\nThis analyzer requires that all uses of idx and s match forms\nthat can be resolved by replacements with the before, after, and ok variables returned by strings.Cut. It also requires that are no modifications or potential modifications to idx, s, and substr.",
+ "Default": "true",
+ "Status": ""
+ },
+ {
"Name": "\"stringscutprefix\"",
"Doc": "replace HasPrefix/TrimPrefix with CutPrefix\n\nThe stringscutprefix analyzer simplifies a common pattern where code first\nchecks for a prefix with `strings.HasPrefix` and then removes it with\n`strings.TrimPrefix`. It replaces this two-step process with a single call\nto `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles\nthe equivalent functions in the `bytes` package.\n\nFor example, this input:\n\n\tif strings.HasPrefix(s, prefix) {\n\t use(strings.TrimPrefix(s, prefix))\n\t}\n\nis fixed to:\n\n\tif after, ok := strings.CutPrefix(s, prefix); ok {\n\t use(after)\n\t}\n\nThe analyzer also offers fixes to use CutSuffix in a similar way.\nThis input:\n\n\tif strings.HasSuffix(s, suffix) {\n\t use(strings.TrimSuffix(s, suffix))\n\t}\n\nis fixed to:\n\n\tif before, ok := strings.CutSuffix(s, suffix); ok {\n\t use(before)\n\t}",
"Default": "true",
@@ -3537,6 +3543,12 @@
"Default": true
},
{
+ "Name": "stringscut",
+ "Doc": "replace strings.Index etc. with strings.Cut\n\nThis analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.\n\nFor example:\n\n\tidx := strings.Index(s, substr)\n\tif i \u003e= 0 {\n\t return s[:idx]\n\t}\n\nis replaced by:\n\n\tbefore, _, ok := strings.Cut(s, substr)\n\tif ok {\n\t return before\n\t}\n\nThis analyzer requires that all uses of idx and s match forms\nthat can be resolved by replacements with the before, after, and ok variables returned by strings.Cut. It also requires that are no modifications or potential modifications to idx, s, and substr.",
+ "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut",
+ "Default": true
+ },
+ {
"Name": "stringscutprefix",
"Doc": "replace HasPrefix/TrimPrefix with CutPrefix\n\nThe stringscutprefix analyzer simplifies a common pattern where code first\nchecks for a prefix with `strings.HasPrefix` and then removes it with\n`strings.TrimPrefix`. It replaces this two-step process with a single call\nto `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles\nthe equivalent functions in the `bytes` package.\n\nFor example, this input:\n\n\tif strings.HasPrefix(s, prefix) {\n\t use(strings.TrimPrefix(s, prefix))\n\t}\n\nis fixed to:\n\n\tif after, ok := strings.CutPrefix(s, prefix); ok {\n\t use(after)\n\t}\n\nThe analyzer also offers fixes to use CutSuffix in a similar way.\nThis input:\n\n\tif strings.HasSuffix(s, suffix) {\n\t use(strings.TrimSuffix(s, suffix))\n\t}\n\nis fixed to:\n\n\tif before, ok := strings.CutSuffix(s, suffix); ok {\n\t use(before)\n\t}",
"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscutprefix",
diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go
index 2bf700b..ab4d9d3 100644
--- a/gopls/internal/settings/analysis.go
+++ b/gopls/internal/settings/analysis.go
@@ -261,6 +261,7 @@
{analyzer: modernize.SlicesDeleteAnalyzer, severity: protocol.SeverityHint, nonDefault: true}, // not nil-preserving
{analyzer: modernize.SlicesSortAnalyzer, severity: protocol.SeverityHint},
{analyzer: modernize.StringsBuilderAnalyzer, severity: protocol.SeverityHint},
+ {analyzer: modernize.StringsCutAnalyzer, severity: protocol.SeverityHint},
{analyzer: modernize.StringsCutPrefixAnalyzer, severity: protocol.SeverityHint},
{analyzer: modernize.StringsSeqAnalyzer, severity: protocol.SeverityHint},
{analyzer: modernize.TestingContextAnalyzer, severity: protocol.SeverityHint},

Change information

Files:
  • M gopls/doc/analyzers.md
  • M gopls/internal/analysis/modernize/doc.go
  • M gopls/internal/analysis/modernize/modernize.go
  • M gopls/internal/analysis/modernize/modernize_test.go
  • A gopls/internal/analysis/modernize/stringscut.go
  • A gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go
  • A gopls/internal/analysis/modernize/testdata/src/stringscut/stringscut.go.golden
  • M gopls/internal/doc/api.json
  • M gopls/internal/settings/analysis.go
Change size: L
Delta: 9 files changed, 845 insertions(+), 0 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
Gerrit-Change-Number: 706936
Gerrit-PatchSet: 1
Gerrit-Owner: Madeline Kalil <mka...@google.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Madeline Kalil (Gerrit)

unread,
Sep 25, 2025, 3:45:30 PMSep 25
to goph...@pubsubhelper.golang.org, Alan Donovan, golang-co...@googlegroups.com
Attention needed from Alan Donovan

Madeline Kalil voted Commit-Queue+1

Commit-Queue+1
Open in Gerrit

Related details

Attention is currently required from:
  • Alan Donovan
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
Gerrit-Change-Number: 706936
Gerrit-PatchSet: 1
Gerrit-Owner: Madeline Kalil <mka...@google.com>
Gerrit-Reviewer: Alan Donovan <adon...@google.com>
Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
Gerrit-Attention: Alan Donovan <adon...@google.com>
Gerrit-Comment-Date: Thu, 25 Sep 2025 19:45:26 +0000
Gerrit-HasComments: No
Gerrit-Has-Labels: Yes
unsatisfied_requirement
satisfied_requirement
open
diffy

Alan Donovan (Gerrit)

unread,
Oct 2, 2025, 2:35:48 PM (13 days ago) Oct 2
to Madeline Kalil, goph...@pubsubhelper.golang.org, Go LUCI, golang-co...@googlegroups.com
Attention needed from Madeline Kalil

Alan Donovan added 32 comments

Patchset-level comments
File-level comment, Patchset 1 (Latest):
Alan Donovan . resolved

Thanks for tackling this problem. Phew there's a lot of code here! I think we can reduce it somewhat and make the flow of concepts clearer by tweaking the boundaries between components a little.

File gopls/internal/analysis/modernize/doc.go
Line 247, Patchset 1 (Latest):# Analyzer stringscut
Alan Donovan . unresolved

The modernize package has moved; you'll need to git mv gopls/internal/analysis/modernize/foo to go/analysis/passes/modernize/foo.

Line 251, Patchset 1 (Latest):This analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.
Alan Donovan . unresolved

How about:

This analyzer replaces certain patterns of use of [strings.Index] and string slicing by [strings.Cut], added in go1.18.

...code...

It also handles variants using [strings.IndexByte] instead of Index, or the bytes package instead of strings.

Fixes are offered only in cases in which there are no potential modifications of the i, s, or substr expressions between their definition and use.

Line 256, Patchset 1 (Latest): if i >= 0 {
Alan Donovan . unresolved

idx

(but better to use i throughout)

File gopls/internal/analysis/modernize/modernize.go
Line 48, Patchset 1 (Latest): StringsCutAnalyzer,
Alan Donovan . unresolved

sort lines (move to L47)

File gopls/internal/analysis/modernize/stringscut.go
Line 41, Patchset 1 (Latest):// stringscut offers a fix to replace an occurence of strings.Index, strings.IndexByte, bytes.Index,
Alan Donovan . unresolved

occurrence

Line 52, Patchset 1 (Latest):// b: s[idx+len(substr):], s[len(substr) + idx:], s[idx + const], s[const + idx] (where const = len(substr))
Alan Donovan . unresolved

k

(since const is a keyword)

Line 58, Patchset 1 (Latest):// Then, the replacement involves the following substitutions:

// 1. Replace "idx := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)"
// 2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok.
// 3. Replace slice expressions (a) with "before" and slice expressions (b) with after.
Alan Donovan . unresolved
An annotated example would illustrate this whole comment more clearly. Present the basic transformation from
```
i := strings.Index(s, substr)
if i >= 0 {
use(s[:i], s[i:])
}
```
to:
```

before, after, ok := strings.Cut(s, substr)
if !ok {
use(s[:idx], s[0:idx]
}
```

and then explain the principles of the variants, for example:

```
The condition must establish that i > -1. Variants include ...etc...
If the condition is negated (e.g. establishes `i < 0`), we use `if ok` instead.
```

This explanation suggests that there should be a function in the implementation that establishes whether `i > -1`, its negation, or neither. That argues for unifying isLessThanZeroCheck and isGreaterEqualToZeroCheck into a single function that returns -1, +1 or zero.

Line 63, Patchset 1 (Latest):
Alan Donovan . unresolved

delete blank (so comment becomes a doc comment)

Line 80, Patchset 1 (Latest): for curFile := range filesUsing(inspect, info, "go1.18") {
Alan Donovan . unresolved

Let's perform the version check at the last moment. Since the version filter is not expected to be very selective, it's more efficient to make a single pass over the whole package than one per file.

			// Check file version.
file := enclosingFile(curCall)
if !fileUses(info, file, "go1.18") {
continue // new(expr) not available in this file
}

isStringsIndex = analysisinternal.IsFunctionNamed(obj, "strings", "Index")
isBytesIndex = analysisinternal.IsFunctionNamed(obj, "bytes", "Index")
isStringsIndexByte = analysisinternal.IsFunctionNamed(obj, "strings", "IndexByte")
isBytesIndexByte = analysisinternal.IsFunctionNamed(obj, "bytes", "IndexByte")
)

if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
continue
}
Alan Donovan . unresolved
Combine: 
```
if !analysisinternal.IsFunctionNamed(obj, "strings", "Index", "IndexByte") &&
!analysisinternal.IsFunctionNamed(obj, "bytes", "Index", "IndexByte") {
```
Line 99, Patchset 1 (Latest): callParent := curCall.Parent()
Alan Donovan . unresolved
You might find this code from CL 706635 is a more direct way to get the variable obj to which cur is assigned:
```
switch ek, idx := cur.ParentEdge(); ek {
case edge.ValueSpec_Values:
curName := cur.Parent().ChildAt(edge.ValueSpec_Names, idx)
if id, ok := curName.Node().(*ast.Ident); ok {
obj = info.Defs[id]
}
case edge.AssignStmt_Rhs:
curLhs := cur.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
if id, ok := curLhs.Node().(*ast.Ident); ok {
obj = info.ObjectOf(id)
}
}
```
Line 100, Patchset 1 (Latest): assign, okAsgn := callParent.Node().(*ast.AssignStmt) // :=
Alan Donovan . unresolved

:= or =

Line 102, Patchset 1 (Latest): if okAsgn || okDecl {
Alan Donovan . unresolved

The body of this if statement is very long, without clear separation of steps. I don't mind long blocks of code (or long functions) so long as the sequence is obvious, but distinct tasks are interleaved here: the recognition of the various forms of `x = f()`, `x := f()`, `var x = f()` assignment and extraction of the LHS symbol; processing of the uses of x; checking for lvalue uses; editing the condition; and editing the assignment.

It would be great if we could separate these tasks more clearly. I notice that we are reformatting the s and substr expressions at L193. What we've learned from other modernizers is that the less we reformat, the better, since it loses comments and vertical space. If we can instead make more surgical edits, it's more likely to produce a cleaner result. For example, rather than rewriting
```
var i = string.Index(s, sub)
--------------------------------------------
var before, after, ok = string.Cut (s, sub)
```
we should aim instead for these edits:
```
var i = string.Index(s, sub)
----------------- -----
var before, after, ok = string.Cut (s, sub)
```

This means we only need to record the positions of the LHS identifier, and the SelectorExpr.Sel from the CallExpr.Fun, and then we can forget about the differences between var and := assignments, leading to a cleaner separation of steps.

Line 135, Patchset 1 (Latest): idxObj := info.Defs[idx]
Alan Donovan . unresolved

idxVar := info.Defs[idx].(*types.Var)

Line 263, Patchset 1 (Latest):// If all uses of idx match one of the valid formats, return a list of occurences for each format.
Alan Donovan . unresolved

What are the valid formats? Some orientation comments would help here.

Line 266, Patchset 1 (Latest):func parseIdxUses(pass *analysis.Pass, uses iter.Seq[inspector.Cursor], idx *ast.Ident, s, substr ast.Expr, afterPos token.Pos) (lessZero, greaterZero, beforeSlice, afterSlice []ast.Expr) {
Alan Donovan . unresolved

"parse" isn't quite right here. (It may be in plain English, but in this domain it always refers to building a tree from a stream of tokens.) `check` would be better.

But we'll need to make sure all the names of stringscut helper functions are specific enough that they don't pollute the modernize package's namespace. The ideal names will hopefully emerge as we iterate on the code organization.

Line 266, Patchset 1 (Latest):func parseIdxUses(pass *analysis.Pass, uses iter.Seq[inspector.Cursor], idx *ast.Ident, s, substr ast.Expr, afterPos token.Pos) (lessZero, greaterZero, beforeSlice, afterSlice []ast.Expr) {
Alan Donovan . unresolved

This should be *types.Var for a local variable; we shouldn't care about the syntax of idx any more. (The caller should pass idxObj aka idxVar.)

All of the uses of equalSyntax on idx should be using resolved symbols, i.e. checking whether the specific symbol is used, not just another symbol whose name happens to be spelled the same way.

Line 268, Patchset 1 (Latest): for c := range cur.Enclosing((*ast.BinaryExpr)(nil), (*ast.SliceExpr)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil), (*ast.GenDecl)(nil)) {
Alan Donovan . unresolved

(I haven't studied this function yet.)

Line 351, Patchset 1 (Latest): case *ast.CallExpr:

// Allow len(substr) and string() conversion, but any other
// CallExpr involving the ident will result in not returning a
// quick fix.

if isCall(n, "len", "string") {
continue
} else {
return true
}
Alan Donovan . unresolved

Passing a value to a function cannot modify it, only things referred to by that value. So for an int or a string (but not `[]byte`), we can ignore calls.

Line 360, Patchset 1 (Latest): case *ast.AssignStmt:
Alan Donovan . unresolved

Assignments are not the only uses of variables; `&v` operations should also be assumed to (ultimately) mutate a variable. See the uses check in stringsbuilder for an example.

Line 370, Patchset 1 (Latest):// Returns true if the BinaryExpr is of the form idx < 0, 0 > idx, idx == -1, or
Alan Donovan . unresolved

The function doesn't actually check the "idx" operand, but it should. Do add test cases before you fix the bugs.

Line 373, Patchset 1 (Latest): return check.Op == token.LSS && typesinternal.IsZeroExpr(check.Y) || check.Op == token.GTR && typesinternal.IsZeroExpr(check.X) || (check.Op == token.EQL && (isNegativeOne(check.X) || isNegativeOne(check.Y)))
Alan Donovan . unresolved

newline

Line 376, Patchset 1 (Latest):// Returns true if the expr is equivalent to -1.
Alan Donovan . unresolved

Doc comments should be sentences with the declared name as their subject:

// isNegativeOne reports whether expr denotes -1.
// It may report false negatives.

Line 389, Patchset 1 (Latest): return check.Op == token.GEQ && typesinternal.IsZeroExpr(check.Y) || check.Op == token.LEQ && typesinternal.IsZeroExpr(check.X)
Alan Donovan . unresolved

It would be more general to ask the type checker whether this expression denotes the constant zero, rather than whether it is spelled "0". We have a helper for that: isZeroIntLiteral.

(BTW, that function's name is a lie since it works on non-literal constants too. Let's rename it to isZeroIntConst.)

Line 399, Patchset 1 (Latest):func isAfterSlice(pass *analysis.Pass, slice *ast.SliceExpr, idx, substr ast.Expr) bool {
Alan Donovan . unresolved

*types.Var

Line 399, Patchset 1 (Latest):func isAfterSlice(pass *analysis.Pass, slice *ast.SliceExpr, idx, substr ast.Expr) bool {
Alan Donovan . unresolved

info *types.Info

Line 402, Patchset 1 (Latest): return false
Alan Donovan . unresolved

We should also reject s[low:high:max] if max is present.

Line 404, Patchset 1 (Latest): // Handle s[idx + const] where substr is a basic lit

// substr could also be a call expr like []byte("str")
// TODO(mkalil): This does not handle other methods used to create byte slices
// like byte literals, or using make/append.
var substrLit *ast.BasicLit
switch n := substr.(type) {
case *ast.BasicLit:
substrLit = n
case *ast.CallExpr:

// Check if call matches []byte
tv := pass.TypesInfo.Types[n.Fun]

if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
// Only one arg in []byte conversion.
if lit, ok := n.Args[0].(*ast.BasicLit); ok {
substrLit = lit
}
}
}
Alan Donovan . unresolved

This concept seems like a useful function abstraction:

```
// stringConst returns the value of the constant string denoted by e, if any,
// and whether it is wrapped in a []byte("...") conversion.
func stringConst(info *types.Info, e ast.Expr) (k constant.Value, byteconv bool)
```
The implementation shouldn't use info.Types and needn't care about BasicLit.

Line 424, Patchset 1 (Latest): substr_len := len(substrLit.Value) - 2 //BasicLit includes beginning and ending quote
Alan Donovan . unresolved

This isn't the correct way to compute the length of the string denoted by a string literal. Use strconv.Unquote.

However, we should be asking the type checker for the constant value of the string so that we don't need to rely on knowledge of how it was denoted (e.g. `"/"` versus `os.PathSeparator`). See L421.

Line 446, Patchset 1 (Latest):func isCall(expr ast.Expr, callName ...string) bool {
Alan Donovan . unresolved

This name is too general for a function this specific at package scope. It should have a more specific name, or, better, be declared locally at L440 as isLenCall.

Line 451, Patchset 1 (Latest): ident, ok := call.Fun.(*ast.Ident)

return ok && len(call.Args) == 1 && slices.Contains(callName, ident.Name)
Alan Donovan . unresolved
```
return typeutil.Callee(info, call) == builtinLen &&
is[*ast.Ident](call.Args[0]) &&
info.Uses(call.Args[0].(*ast.Ident)) == idx
```
and then you don't need equalSyntax.
Open in Gerrit

Related details

Attention is currently required from:
  • Madeline Kalil
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 1
    Gerrit-Owner: Madeline Kalil <mka...@google.com>
    Gerrit-Reviewer: Alan Donovan <adon...@google.com>
    Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
    Gerrit-Attention: Madeline Kalil <mka...@google.com>
    Gerrit-Comment-Date: Thu, 02 Oct 2025 18:35:43 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 8, 2025, 12:56:12 PM (7 days ago) Oct 8
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Madeline Kalil

    Madeline Kalil uploaded new patchset

    Madeline Kalil uploaded patch set #2 to this change.
    Following approvals got outdated and were removed:
    • TryBots-Pass: LUCI-TryBot-Result-1 by Go LUCI
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Madeline Kalil
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 2
    unsatisfied_requirement
    open
    diffy

    Mateusz Poliwczak (Gerrit)

    unread,
    Oct 12, 2025, 1:01:48 PM (3 days ago) Oct 12
    to Madeline Kalil, goph...@pubsubhelper.golang.org, Go LUCI, Alan Donovan, golang-co...@googlegroups.com
    Attention needed from Madeline Kalil

    Mateusz Poliwczak added 1 comment

    File go/analysis/passes/modernize/stringscut.go
    Line 27, Patchset 2 (Latest):var StringsCutAnalyzer = &analysis.Analyzer{
    Mateusz Poliwczak . unresolved

    Unexport? Since it should go through proposal review.

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Madeline Kalil
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 2
    Gerrit-Owner: Madeline Kalil <mka...@google.com>
    Gerrit-Reviewer: Alan Donovan <adon...@google.com>
    Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
    Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Madeline Kalil <mka...@google.com>
    Gerrit-Comment-Date: Sun, 12 Oct 2025 17:01:41 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 10:41:24 AM (3 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Madeline Kalil

    Madeline Kalil uploaded new patchset

    Madeline Kalil uploaded patch set #3 to this change.
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Madeline Kalil
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 3
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 10:51:44 AM (3 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Madeline Kalil

    Madeline Kalil uploaded new patchset

    Madeline Kalil uploaded patch set #4 to this change.
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Madeline Kalil
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 4
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 4:35:18 PM (2 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Madeline Kalil

    Madeline Kalil uploaded new patchset

    Madeline Kalil uploaded patch set #6 to this change.
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Madeline Kalil
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 6
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 4:37:08 PM (2 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, Mateusz Poliwczak, Go LUCI, Alan Donovan, golang-co...@googlegroups.com
    Attention needed from Alan Donovan and Mateusz Poliwczak

    Madeline Kalil voted and added 30 comments

    Votes added by Madeline Kalil

    Commit-Queue+1

    30 comments

    Patchset-level comments
    File-level comment, Patchset 6 (Latest):
    Madeline Kalil . resolved

    Thank you very much for the review Alan, and sorry this CL is so big!

    File go/analysis/passes/modernize/stringscut.go
    Line 27, Patchset 2:var StringsCutAnalyzer = &analysis.Analyzer{
    Mateusz Poliwczak . resolved

    Unexport? Since it should go through proposal review.

    Madeline Kalil

    Done

    File gopls/internal/analysis/modernize/doc.go
    Line 247, Patchset 1:# Analyzer stringscut
    Alan Donovan . resolved

    The modernize package has moved; you'll need to git mv gopls/internal/analysis/modernize/foo to go/analysis/passes/modernize/foo.

    Madeline Kalil

    Done

    Line 251, Patchset 1:This analyzer replaces instances of strings.Index, strings.IndexByte, bytes.Index, and bytes.IndexByte with strings.Cut or bytes.Cut.
    Alan Donovan . resolved

    How about:

    This analyzer replaces certain patterns of use of [strings.Index] and string slicing by [strings.Cut], added in go1.18.

    ...code...

    It also handles variants using [strings.IndexByte] instead of Index, or the bytes package instead of strings.

    Fixes are offered only in cases in which there are no potential modifications of the i, s, or substr expressions between their definition and use.

    Madeline Kalil

    Done

    Line 256, Patchset 1: if i >= 0 {
    Alan Donovan . resolved

    idx

    (but better to use i throughout)

    Madeline Kalil

    Done

    File gopls/internal/analysis/modernize/modernize.go
    Line 48, Patchset 1: StringsCutAnalyzer,
    Alan Donovan . resolved

    sort lines (move to L47)

    Madeline Kalil

    Done

    File gopls/internal/analysis/modernize/stringscut.go
    Line 41, Patchset 1:// stringscut offers a fix to replace an occurence of strings.Index, strings.IndexByte, bytes.Index,
    Alan Donovan . resolved

    occurrence

    Madeline Kalil

    Done

    Line 52, Patchset 1:// b: s[idx+len(substr):], s[len(substr) + idx:], s[idx + const], s[const + idx] (where const = len(substr))
    Alan Donovan . resolved

    k

    (since const is a keyword)

    Madeline Kalil

    Done

    Line 58, Patchset 1:// Then, the replacement involves the following substitutions:

    // 1. Replace "idx := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)"
    // 2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok.
    // 3. Replace slice expressions (a) with "before" and slice expressions (b) with after.
    Alan Donovan . resolved
    An annotated example would illustrate this whole comment more clearly. Present the basic transformation from
    ```
    i := strings.Index(s, substr)
    if i >= 0 {
    use(s[:i], s[i:])
    }
    ```
    to:
    ```
    before, after, ok := strings.Cut(s, substr)
    if !ok {
    use(s[:idx], s[0:idx]
    }
    ```

    and then explain the principles of the variants, for example:

    ```
    The condition must establish that i > -1. Variants include ...etc...
    If the condition is negated (e.g. establishes `i < 0`), we use `if ok` instead.
    ```

    This explanation suggests that there should be a function in the implementation that establishes whether `i > -1`, its negation, or neither. That argues for unifying isLessThanZeroCheck and isGreaterEqualToZeroCheck into a single function that returns -1, +1 or zero.

    Madeline Kalil

    Done

    Line 63, Patchset 1:
    Alan Donovan . resolved

    delete blank (so comment becomes a doc comment)

    Madeline Kalil

    Done

    Line 80, Patchset 1: for curFile := range filesUsing(inspect, info, "go1.18") {
    Alan Donovan . resolved

    Let's perform the version check at the last moment. Since the version filter is not expected to be very selective, it's more efficient to make a single pass over the whole package than one per file.

    			// Check file version.
    file := enclosingFile(curCall)
    if !fileUses(info, file, "go1.18") {
    continue // new(expr) not available in this file
    }
    Madeline Kalil

    Done


    isStringsIndex = analysisinternal.IsFunctionNamed(obj, "strings", "Index")
    isBytesIndex = analysisinternal.IsFunctionNamed(obj, "bytes", "Index")
    isStringsIndexByte = analysisinternal.IsFunctionNamed(obj, "strings", "IndexByte")
    isBytesIndexByte = analysisinternal.IsFunctionNamed(obj, "bytes", "IndexByte")
    )
    if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
    continue
    }
    Alan Donovan . unresolved
    Combine: 
    ```
    if !analysisinternal.IsFunctionNamed(obj, "strings", "Index", "IndexByte") &&
    !analysisinternal.IsFunctionNamed(obj, "bytes", "Index", "IndexByte") {
    ```
    Madeline Kalil

    I need the individual cases to form the analyzer message like "strings.Index can be replaced with strings.Cut".

    Line 99, Patchset 1: callParent := curCall.Parent()
    Alan Donovan . resolved
    You might find this code from CL 706635 is a more direct way to get the variable obj to which cur is assigned:
    ```
    switch ek, idx := cur.ParentEdge(); ek {
    case edge.ValueSpec_Values:
    curName := cur.Parent().ChildAt(edge.ValueSpec_Names, idx)
    if id, ok := curName.Node().(*ast.Ident); ok {
    obj = info.Defs[id]
    }
    case edge.AssignStmt_Rhs:
    curLhs := cur.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
    if id, ok := curLhs.Node().(*ast.Ident); ok {
    obj = info.ObjectOf(id)
    }
    }
    ```
    Madeline Kalil

    Done

    Line 100, Patchset 1: assign, okAsgn := callParent.Node().(*ast.AssignStmt) // :=
    Alan Donovan . resolved

    := or =

    Madeline Kalil

    Done

    Line 102, Patchset 1: if okAsgn || okDecl {
    Alan Donovan . resolved

    The body of this if statement is very long, without clear separation of steps. I don't mind long blocks of code (or long functions) so long as the sequence is obvious, but distinct tasks are interleaved here: the recognition of the various forms of `x = f()`, `x := f()`, `var x = f()` assignment and extraction of the LHS symbol; processing of the uses of x; checking for lvalue uses; editing the condition; and editing the assignment.

    It would be great if we could separate these tasks more clearly. I notice that we are reformatting the s and substr expressions at L193. What we've learned from other modernizers is that the less we reformat, the better, since it loses comments and vertical space. If we can instead make more surgical edits, it's more likely to produce a cleaner result. For example, rather than rewriting
    ```
    var i = string.Index(s, sub)
    --------------------------------------------
    var before, after, ok = string.Cut (s, sub)
    ```
    we should aim instead for these edits:
    ```
    var i = string.Index(s, sub)
    ----------------- -----
    var before, after, ok = string.Cut (s, sub)
    ```

    This means we only need to record the positions of the LHS identifier, and the SelectorExpr.Sel from the CallExpr.Fun, and then we can forget about the differences between var and := assignments, leading to a cleaner separation of steps.

    Madeline Kalil

    Done

    Line 135, Patchset 1: idxObj := info.Defs[idx]
    Alan Donovan . resolved

    idxVar := info.Defs[idx].(*types.Var)

    Madeline Kalil

    Done

    Line 263, Patchset 1:// If all uses of idx match one of the valid formats, return a list of occurences for each format.
    Alan Donovan . resolved

    What are the valid formats? Some orientation comments would help here.

    Madeline Kalil

    Done

    Line 266, Patchset 1:func parseIdxUses(pass *analysis.Pass, uses iter.Seq[inspector.Cursor], idx *ast.Ident, s, substr ast.Expr, afterPos token.Pos) (lessZero, greaterZero, beforeSlice, afterSlice []ast.Expr) {
    Alan Donovan . resolved

    This should be *types.Var for a local variable; we shouldn't care about the syntax of idx any more. (The caller should pass idxObj aka idxVar.)

    All of the uses of equalSyntax on idx should be using resolved symbols, i.e. checking whether the specific symbol is used, not just another symbol whose name happens to be spelled the same way.

    Madeline Kalil

    Thanks, fixed all uses of EqualSyntax. I now have two helpers with different arguments that both use type information to get the idents' objects and see if they are the same

    Line 351, Patchset 1: case *ast.CallExpr:

    // Allow len(substr) and string() conversion, but any other
    // CallExpr involving the ident will result in not returning a
    // quick fix.
    if isCall(n, "len", "string") {
    continue
    } else {
    return true
    }
    Alan Donovan . resolved

    Passing a value to a function cannot modify it, only things referred to by that value. So for an int or a string (but not `[]byte`), we can ignore calls.

    Madeline Kalil

    I changed this function a bit based on the example of inspecting Uses in stringsbuilder

    Line 360, Patchset 1: case *ast.AssignStmt:
    Alan Donovan . resolved

    Assignments are not the only uses of variables; `&v` operations should also be assumed to (ultimately) mutate a variable. See the uses check in stringsbuilder for an example.

    Madeline Kalil

    Done

    Line 370, Patchset 1:// Returns true if the BinaryExpr is of the form idx < 0, 0 > idx, idx == -1, or
    Alan Donovan . unresolved

    The function doesn't actually check the "idx" operand, but it should. Do add test cases before you fix the bugs.

    Madeline Kalil

    I think at this point we can assume that one of the operands is "idx", since we're only checking expressions that involve uses of idx.

    Line 373, Patchset 1: return check.Op == token.LSS && typesinternal.IsZeroExpr(check.Y) || check.Op == token.GTR && typesinternal.IsZeroExpr(check.X) || (check.Op == token.EQL && (isNegativeOne(check.X) || isNegativeOne(check.Y)))
    Alan Donovan . resolved

    newline

    Madeline Kalil

    Done

    Line 376, Patchset 1:// Returns true if the expr is equivalent to -1.
    Alan Donovan . resolved

    Doc comments should be sentences with the declared name as their subject:

    // isNegativeOne reports whether expr denotes -1.
    // It may report false negatives.

    Madeline Kalil

    Done

    Line 389, Patchset 1: return check.Op == token.GEQ && typesinternal.IsZeroExpr(check.Y) || check.Op == token.LEQ && typesinternal.IsZeroExpr(check.X)
    Alan Donovan . resolved

    It would be more general to ask the type checker whether this expression denotes the constant zero, rather than whether it is spelled "0". We have a helper for that: isZeroIntLiteral.

    (BTW, that function's name is a lie since it works on non-literal constants too. Let's rename it to isZeroIntConst.)

    Madeline Kalil

    Done

    Line 399, Patchset 1:func isAfterSlice(pass *analysis.Pass, slice *ast.SliceExpr, idx, substr ast.Expr) bool {
    Alan Donovan . resolved

    *types.Var

    Madeline Kalil

    Done

    Line 399, Patchset 1:func isAfterSlice(pass *analysis.Pass, slice *ast.SliceExpr, idx, substr ast.Expr) bool {
    Alan Donovan . resolved

    info *types.Info

    Madeline Kalil

    Done

    Line 402, Patchset 1: return false
    Alan Donovan . resolved

    We should also reject s[low:high:max] if max is present.

    Madeline Kalil

    Done

    Line 404, Patchset 1: // Handle s[idx + const] where substr is a basic lit

    // substr could also be a call expr like []byte("str")
    // TODO(mkalil): This does not handle other methods used to create byte slices
    // like byte literals, or using make/append.
    var substrLit *ast.BasicLit
    switch n := substr.(type) {
    case *ast.BasicLit:
    substrLit = n
    case *ast.CallExpr:
    // Check if call matches []byte
    tv := pass.TypesInfo.Types[n.Fun]
    if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
    // Only one arg in []byte conversion.
    if lit, ok := n.Args[0].(*ast.BasicLit); ok {
    substrLit = lit
    }
    }
    }
    Alan Donovan . unresolved

    This concept seems like a useful function abstraction:

    ```
    // stringConst returns the value of the constant string denoted by e, if any,
    // and whether it is wrapped in a []byte("...") conversion.
    func stringConst(info *types.Info, e ast.Expr) (k constant.Value, byteconv bool)
    ```
    The implementation shouldn't use info.Types and needn't care about BasicLit.

    Madeline Kalil

    A bit confused about this, will ask you in person

    Line 446, Patchset 1:func isCall(expr ast.Expr, callName ...string) bool {
    Alan Donovan . resolved

    This name is too general for a function this specific at package scope. It should have a more specific name, or, better, be declared locally at L440 as isLenCall.

    Madeline Kalil

    Done

    Line 451, Patchset 1: ident, ok := call.Fun.(*ast.Ident)

    return ok && len(call.Args) == 1 && slices.Contains(callName, ident.Name)
    Alan Donovan . resolved
    ```
    return typeutil.Callee(info, call) == builtinLen &&
    is[*ast.Ident](call.Args[0]) &&
    info.Uses(call.Args[0].(*ast.Ident)) == idx
    ```
    and then you don't need equalSyntax.
    Madeline Kalil

    Done

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Alan Donovan
    • Mateusz Poliwczak
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 6
    Gerrit-Owner: Madeline Kalil <mka...@google.com>
    Gerrit-Reviewer: Alan Donovan <adon...@google.com>
    Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
    Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Alan Donovan <adon...@google.com>
    Gerrit-Comment-Date: Mon, 13 Oct 2025 20:37:04 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: Yes
    Comment-In-Reply-To: Mateusz Poliwczak <mpoliw...@gmail.com>
    Comment-In-Reply-To: Alan Donovan <adon...@google.com>
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 4:48:17 PM (2 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
    Attention needed from Alan Donovan, Madeline Kalil and Mateusz Poliwczak

    Madeline Kalil uploaded new patchset

    Madeline Kalil uploaded patch set #7 to this change.
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Alan Donovan
    • Madeline Kalil
    • Mateusz Poliwczak
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: newpatchset
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 7
    Gerrit-Owner: Madeline Kalil <mka...@google.com>
    Gerrit-Reviewer: Alan Donovan <adon...@google.com>
    Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
    Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Madeline Kalil <mka...@google.com>
    unsatisfied_requirement
    open
    diffy

    Madeline Kalil (Gerrit)

    unread,
    Oct 13, 2025, 4:48:32 PM (2 days ago) Oct 13
    to goph...@pubsubhelper.golang.org, Go LUCI, Mateusz Poliwczak, Alan Donovan, golang-co...@googlegroups.com
    Attention needed from Alan Donovan and Mateusz Poliwczak

    Madeline Kalil voted Commit-Queue+1

    Commit-Queue+1
    Open in Gerrit

    Related details

    Attention is currently required from:
    • Alan Donovan
    • Mateusz Poliwczak
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
    Gerrit-Change-Number: 706936
    Gerrit-PatchSet: 7
    Gerrit-Owner: Madeline Kalil <mka...@google.com>
    Gerrit-Reviewer: Alan Donovan <adon...@google.com>
    Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
    Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Mateusz Poliwczak <mpoliw...@gmail.com>
    Gerrit-Attention: Alan Donovan <adon...@google.com>
    Gerrit-Comment-Date: Mon, 13 Oct 2025 20:48:28 +0000
    Gerrit-HasComments: No
    Gerrit-Has-Labels: Yes
    unsatisfied_requirement
    open
    diffy

    Ingo Oeser (Gerrit)

    unread,
    Oct 14, 2025, 9:49:24 AM (yesterday) Oct 14
    to Madeline Kalil, goph...@pubsubhelper.golang.org, Go LUCI, Mateusz Poliwczak, Alan Donovan, golang-co...@googlegroups.com
    Attention needed from Alan Donovan, Madeline Kalil and Mateusz Poliwczak

    Ingo Oeser added 5 comments

    Patchset-level comments
    File-level comment, Patchset 7 (Latest):
    Ingo Oeser . resolved

    In a few places strings.Cut is overkill and strings.Contains will be clearer code.

    Every time none of the two result slices are needed, strings.Cut usage can be simplified to strings.Contains to have clearer code.


    Please either exclude those cases or replace them with strings.Contains.

    File go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go
    Line 12, Patchset 7 (Latest):}
    Ingo Oeser . unresolved

    This use case is better replaced by strings.Contains instead of strings.Cut

    Line 17, Patchset 7 (Latest):}
    Ingo Oeser . unresolved

    Also here strings.Contains(s, "+") would be the better replacement.

    Line 58, Patchset 7 (Latest):}
    Ingo Oeser . unresolved

    Also here: strings.Contains is the clearer replacement.

    Line 68, Patchset 7 (Latest): return true
    Ingo Oeser . unresolved

    strings.Contains is better here as well.

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Alan Donovan
    • Madeline Kalil
    • Mateusz Poliwczak
    Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedNo-Unresolved-Comments
      • requirement is not satisfiedReview-Enforcement
      • requirement satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: tools
      Gerrit-Branch: master
      Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
      Gerrit-Change-Number: 706936
      Gerrit-PatchSet: 7
      Gerrit-Owner: Madeline Kalil <mka...@google.com>
      Gerrit-Reviewer: Alan Donovan <adon...@google.com>
      Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
      Gerrit-CC: Ingo Oeser <night...@googlemail.com>
      Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
      Gerrit-Attention: Madeline Kalil <mka...@google.com>
      Gerrit-Attention: Mateusz Poliwczak <mpoliw...@gmail.com>
      Gerrit-Attention: Alan Donovan <adon...@google.com>
      Gerrit-Comment-Date: Tue, 14 Oct 2025 13:49:15 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      unsatisfied_requirement
      satisfied_requirement
      open
      diffy

      Alan Donovan (Gerrit)

      unread,
      Oct 14, 2025, 10:59:03 AM (yesterday) Oct 14
      to Madeline Kalil, goph...@pubsubhelper.golang.org, Ingo Oeser, Go LUCI, Mateusz Poliwczak, golang-co...@googlegroups.com
      Attention needed from Madeline Kalil and Mateusz Poliwczak

      Alan Donovan added 28 comments

      File go/analysis/passes/modernize/stringscut.go
      Alan Donovan . unresolved

      delete blank

      Line 105, Patchset 7 (Latest):
      nextcall:
      for curCall := range inspect.Root().Preorder((*ast.CallExpr)(nil)) {
      // Check file version.
      file := astutil.EnclosingFile(curCall)

      if !fileUses(info, file, "go1.18") {
      continue // strings.Index not available in this file
      }
      indexCall := curCall.Node().(*ast.CallExpr)
      obj := typeutil.Callee(info, indexCall)
      if obj == nil {
      continue
      }
      var (
      isStringsIndex = typesinternal.IsFunctionNamed(obj, "strings", "Index")
      isBytesIndex = typesinternal.IsFunctionNamed(obj, "bytes", "Index")
      isStringsIndexByte = typesinternal.IsFunctionNamed(obj, "strings", "IndexByte")
      isBytesIndexByte = typesinternal.IsFunctionNamed(obj, "bytes", "IndexByte")

      )
      if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
      continue
      }
      Alan Donovan . unresolved
      Rather than looking at every call in the package and testing (four times) whether it is one of the four callees we care about, it would be much more efficient to use the index to report just the handful of calls that we want:
      ```
      for _, obj := range []types.Object{
      index.Object("strings", "Index"),
      index.Object("strings", "IndexByte"),
      index.Object("bytes", "Index"),
      index.Object("bytes", "IndexByte"),
      } {
      // (obj may be nil)
      nextcall:
      for curCall := range index.Calls(obj) {
      ```
      Line 128, Patchset 7 (Latest): var idxObj types.Object
      Alan Donovan . unresolved

      It's confusing that we have `index` (a type index), strings.Index (a function in the target code), `idx` (a ParentEdge index), and `i` (a local variable in the target code). It would help to be very consistent about the differences between the last three. I propose we use the prefix i (e.g. iIdent, iVar) for the local variable, and `i` in all our code examples.

      I think this block can be simplified to:
      ```
      var iIdent *ast.Ident // defining identifier of i var
      switch ek, idx := curCall.ParentEdge(); ek {
      case edge.ValueSpec_Values:
      // Have: var i = strings.Index(...)
      curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
      iIdent = curName.Node().(*ast.Ident)
      case edge.AssignStmt_Rhs:
      // Have: i := strings.Index(...)
      // (Must be i's definition.)
      curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
      iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil
      }
      if iIdent == nil {
      continue
      }
      iVar, _ := info.Defs[iIdent].(*types.Var)
      if iVar == nil {
      continue
      }
      // Inv: iIdent is i's definition.
      ```

      Note that this asserts that i is a var, and also rejects non-defining uses of i as in `var i int; i = strings.Index(...)`, which I think is important for correctness; see comment at L178.

      Line 140, Patchset 7 (Latest): idxObj = info.ObjectOf(id)
      Alan Donovan . unresolved

      idxVar, _ = info.Defs[id].(*types.Var)
      We know it must be var. And we don't want to continue if

      Line 144, Patchset 7 (Latest): selExpr, ok := indexCall.Fun.(*ast.SelectorExpr)
      Alan Donovan . unresolved

      ast.Unparen(...).(T)

      Always call Unparen before any kind of type switch to remove irrelevant parens.

      But see next comment.

      Line 145, Patchset 7 (Latest): if !ok {
      continue
      }
      Alan Donovan . unresolved

      It is actually possible to call strings.Index without a selector, if the strings package is dot imported. In that case, Fun is only an `*ast.Ident` here. Fortunately we have the `typesinternal.UsedIdent(indexCall.Fun)` helper which will take care of these details and return the ident you care about.

      Line 159, Patchset 7 (Latest): idxVar, ok := info.Defs[idx].(*types.Var)
      Alan Donovan . unresolved

      Isn't this the same as idxObj? We already did the Defs lookup.

      Line 178, Patchset 7 (Latest): afterPos := indexCall.Pos() // don't consider uses before the call to strings.Index etc.
      Alan Donovan . unresolved
      Strictly, we can't ignore uses before the call. Consider
      ```
      var index int
      f(&index)
      index = strings.Index(s, ...)
      g()
      if index >= 0 {
      use(s[:index])
      }
      ```
      In this example, f could escape a pointer to index (e.g. by storing it in a global) and then the call to g() could mutate index through that pointer. So any l-value reference to index other than its strings.Index assignment is potentially problematic.
      I think the simplest thing to do is to handle only cases where index is declared and assigned to strings.Index at the same time, for example:
      ```
      var index = strings.Index(...)
      ```
      or:
      ```
      index := strings.Index(...)
      ```
      but not:
      ```
      var index int
      index, zero := strings.Index(...), 0
      ```
      And the simplest way to detect that is for the logic at L129-143 to assert that the identifier is a def and skip otherwise:

      ```
      switch ek, i := curCall.ParentEdge(); ek {
      case edge.ValueSpec_Values:
      ...
      indexVar, _ = info.Defs[id].(*types.Var)
      }
      if indexVar == nil {
      continue
      }
      ```

      Then you don't need to worry about afterPos here, you can simply check all uses.

      Line 182, Patchset 7 (Latest): if sId, ok := id.(*ast.Ident); ok {
      Alan Donovan . unresolved

      This is just the easy case. What if s or substr are not mere identifiers, but, say, expressions of the form `ptr.field[i]`? Then the problem of proving that the value of the expression is the same each time it is evaluated becomes much more challenging. We need to either (a) reject the candidate if !ok, or (b) analyze s and substr to see whether they are referentially transparent, and if not, analyze all code between declaration and use and see if there are statements or expressions with potential side effects. Obviously (a) is much easier than (b), and seems like a good first step, but it could reject a significant fraction of candidates.

      Line 197, Patchset 7 (Latest): var textEdits []analysis.TextEdit
      Alan Donovan . unresolved

      edits

      Line 247, Patchset 7 (Latest): Pos: indexCall.Pos(),
      Alan Donovan . unresolved

      Rather than report the whole call, let's just flag the strings.Index portion, i.e. call.Fun.

      Line 249, Patchset 7 (Latest): Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
      Alan Donovan . unresolved

      Let's aim for "strings.Index call can be simplified using Cut"

      There's no need to repeat the package name.

      Line 252, Patchset 7 (Latest): Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
      Alan Donovan . unresolved

      This should be a verb phrase describing the fix, e.g. "Simplify %s call using %s".

      Line 274, Patchset 7 (Latest): for c := range cur.Enclosing((*ast.BinaryExpr)(nil), (*ast.SliceExpr)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil), (*ast.GenDecl)(nil)) {
      Alan Donovan . unresolved

      This will report all enclosing nodes of the specified types. For ancestors above the parent, we have no reason to believe that any of their operands is a use of the `index` var. Perhaps you meant simply to call Parent and then type switch?

      Line 311, Patchset 7 (Latest): case *ast.AssignStmt:

      // If the value of idx is reassigned, don't suggest a fix.
      assign := n

      for _, expr := range assign.Lhs {
      if resolvesToSameObj(info, idx, expr) {

      return nil, nil, nil, nil
      }
      }
      // If an alias to idx is created, don't suggest a fix.
      for _, expr := range assign.Rhs {
      if resolvesToSameObj(info, idx, expr) {

      return nil, nil, nil, nil
      }
      }
      case *ast.GenDecl:

      // If an alias to idx is created, don't suggest a fix.
      decl := n

      for i, spec := range decl.Specs {
      if valueSpec, ok := spec.(*ast.ValueSpec); ok {
      if resolvesToSameObj(info, idx, valueSpec.Values[i]) {

      return nil, nil, nil, nil
      }
      }
      }
      case *ast.CallExpr:

      // Don't suggest a fix if idx is passed as an argument to some
      // other function (unless it's the call to strings.Index).
      call := n

      for _, arg := range call.Args {
      if resolvesToSameObj(info, idx, arg) {

      return nil, nil, nil, nil
      }
      }
      }
      Alan Donovan . unresolved

      I wonder why it is necessary to check for assignments here. If we're trying to establish that all uses of the index var appear in a few special forms such as `index < 0`, then shouldn't _any_ use that doesn't match cause this operation to fail?

      Line 377, Patchset 7 (Latest):// checkValue reports whether the check establishes that idx < 0 or idx > -1
      Alan Donovan . unresolved

      Helper functions tailored for stringscut should use names that make sense in the wider context of the modernize package. (Go has no file-local func declarations, even though that would sometimes be useful.)

      Line 380, Patchset 7 (Latest):// inv: a check passed to checkValue should have idx as one of its operands.
      Alan Donovan . unresolved

      has

      Line 381, Patchset 7 (Latest):func checkValue(info *types.Info, check *ast.BinaryExpr) int {
      Alan Donovan . resolved

      There might be a nice general abstraction here over expressions that returns the non-constant operand (here assumed to be idx) along with its sign (-1, 0, +1), or (nil, 0) if the sign cannot be deduced. For another day.

      Line 385, Patchset 7 (Latest): check.Op == token.GTR && isZeroIntConst(info, check.X) || (check.Op == token.EQL &&
      Alan Donovan . unresolved

      linebreak

      Line 391, Patchset 7 (Latest): if check.Op == token.GEQ && isZeroIntConst(info, check.Y) || check.Op == token.LEQ && isZeroIntConst(info, check.X) ||
      Alan Donovan . unresolved

      linebreak

      Line 392, Patchset 7 (Latest): check.Op == token.EQL && isZeroIntConst(info, check.X) || isZeroIntConst(info, check.Y) && check.Op == token.EQL {
      Alan Donovan . unresolved

      linebreak

      Line 392, Patchset 7 (Latest): check.Op == token.EQL && isZeroIntConst(info, check.X) || isZeroIntConst(info, check.Y) && check.Op == token.EQL {
      Alan Donovan . unresolved

      flip the operands (...Op... && ...Y...) for consistency.

      Line 401, Patchset 7 (Latest): switch e := expr.(type) {
      Alan Donovan . unresolved

      return isIntConst(info, e, -1)

      Rename to isMinusOneIntConst to follow the pattern. Alternatively declare it locally within checkValue.

      Line 414, Patchset 7 (Latest): return slice.Low == nil || isZeroIntConst(info, slice.Low)
      Alan Donovan . unresolved

      Don't forget to check that the slice.Max operand (`s[::max]`) is also absent.

      File gopls/internal/analysis/modernize/stringscut.go
      Line 80, Patchset 1: for curFile := range filesUsing(inspect, info, "go1.18") {
      Alan Donovan . unresolved

      Let's perform the version check at the last moment. Since the version filter is not expected to be very selective, it's more efficient to make a single pass over the whole package than one per file.

      			// Check file version.
      file := enclosingFile(curCall)
      if !fileUses(info, file, "go1.18") {
      continue // new(expr) not available in this file
      }
      Madeline Kalil

      Done

      Alan Donovan

      Am I looking at the latest draft? This check is still done eagerly.

      Line 87, Patchset 1: var (
      isStringsIndex = analysisinternal.IsFunctionNamed(obj, "strings", "Index")
      isBytesIndex = analysisinternal.IsFunctionNamed(obj, "bytes", "Index")
      isStringsIndexByte = analysisinternal.IsFunctionNamed(obj, "strings", "IndexByte")
      isBytesIndexByte = analysisinternal.IsFunctionNamed(obj, "bytes", "IndexByte")
      )
      if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
      continue
      }
      Alan Donovan . unresolved
      Combine: 
      ```
      if !analysisinternal.IsFunctionNamed(obj, "strings", "Index", "IndexByte") &&
      !analysisinternal.IsFunctionNamed(obj, "bytes", "Index", "IndexByte") {
      ```
      Madeline Kalil

      I need the individual cases to form the analyzer message like "strings.Index can be replaced with strings.Cut".

      Alan Donovan

      Right, but you don't need one line for each case of the cross product of {strings, bytes} x {Index, IndexByte}. The 10 lines of code that define (replace, with) can be eliminated by computing the string directly from the value of obj in a single expression; see L249.

      Line 102, Patchset 1: if okAsgn || okDecl {
      Alan Donovan . unresolved

      The body of this if statement is very long, without clear separation of steps. I don't mind long blocks of code (or long functions) so long as the sequence is obvious, but distinct tasks are interleaved here: the recognition of the various forms of `x = f()`, `x := f()`, `var x = f()` assignment and extraction of the LHS symbol; processing of the uses of x; checking for lvalue uses; editing the condition; and editing the assignment.

      It would be great if we could separate these tasks more clearly. I notice that we are reformatting the s and substr expressions at L193. What we've learned from other modernizers is that the less we reformat, the better, since it loses comments and vertical space. If we can instead make more surgical edits, it's more likely to produce a cleaner result. For example, rather than rewriting
      ```
      var i = string.Index(s, sub)
      --------------------------------------------
      var before, after, ok = string.Cut (s, sub)
      ```
      we should aim instead for these edits:
      ```
      var i = string.Index(s, sub)
      ----------------- -----
      var before, after, ok = string.Cut (s, sub)
      ```

      This means we only need to record the positions of the LHS identifier, and the SelectorExpr.Sel from the CallExpr.Fun, and then we can forget about the differences between var and := assignments, leading to a cleaner separation of steps.

      Madeline Kalil

      Done

      Alan Donovan

      Please use the aligned before/after notation in my comment above to document each set of TextEdits.

      Line 249, Patchset 1: Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
      Alan Donovan . unresolved
      ```
      Sprintf("%s can be replaced with %s.Cut",
      obj.Pkg().Name(), obj.Name(),
      obj.Pkg().Name())
      ```

      then delete LL236-245.
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Madeline Kalil
      • Mateusz Poliwczak
      Submit Requirements:
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedNo-Unresolved-Comments
      • requirement is not satisfiedReview-Enforcement
      • requirement satisfiedTryBots-Pass
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: tools
      Gerrit-Branch: master
      Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
      Gerrit-Change-Number: 706936
      Gerrit-PatchSet: 7
      Gerrit-Owner: Madeline Kalil <mka...@google.com>
      Gerrit-Reviewer: Alan Donovan <adon...@google.com>
      Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
      Gerrit-CC: Ingo Oeser <night...@googlemail.com>
      Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
      Gerrit-Attention: Madeline Kalil <mka...@google.com>
      Gerrit-Attention: Mateusz Poliwczak <mpoliw...@gmail.com>
      Gerrit-Comment-Date: Tue, 14 Oct 2025 14:58:59 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Madeline Kalil <mka...@google.com>
      Comment-In-Reply-To: Alan Donovan <adon...@google.com>
      unsatisfied_requirement
      satisfied_requirement
      open
      diffy

      Madeline Kalil (Gerrit)

      unread,
      5:10 PM (7 hours ago) 5:10 PM
      to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com
      Attention needed from Madeline Kalil and Mateusz Poliwczak

      Madeline Kalil uploaded new patchset

      Madeline Kalil uploaded patch set #8 to this change.
      Following approvals got outdated and were removed:
      • TryBots-Pass: LUCI-TryBot-Result+1 by Go LUCI
      Open in Gerrit

      Related details

      Attention is currently required from:
      • Madeline Kalil
      • Mateusz Poliwczak
      Submit Requirements:
        • requirement is not satisfiedCode-Review
        • requirement is not satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement is not satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: newpatchset
        Gerrit-Project: tools
        Gerrit-Branch: master
        Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
        Gerrit-Change-Number: 706936
        Gerrit-PatchSet: 8
        unsatisfied_requirement
        open
        diffy

        Madeline Kalil (Gerrit)

        unread,
        5:10 PM (7 hours ago) 5:10 PM
        to goph...@pubsubhelper.golang.org, Ingo Oeser, Go LUCI, Mateusz Poliwczak, Alan Donovan, golang-co...@googlegroups.com

        Madeline Kalil voted and added 18 comments

        Votes added by Madeline Kalil

        Commit-Queue+1

        18 comments

        File go/analysis/passes/modernize/stringscut.go
        Line 89, Patchset 6:
        Alan Donovan . resolved

        delete blank

        Madeline Kalil

        Sorry, I think I accidentally resolved the other comment to do this.


        nextcall:
        for curCall := range inspect.Root().Preorder((*ast.CallExpr)(nil)) {
        // Check file version.
        file := astutil.EnclosingFile(curCall)
        if !fileUses(info, file, "go1.18") {
        continue // strings.Index not available in this file
        }
        indexCall := curCall.Node().(*ast.CallExpr)
        obj := typeutil.Callee(info, indexCall)
        if obj == nil {
        continue
        }
        var (
        isStringsIndex = typesinternal.IsFunctionNamed(obj, "strings", "Index")
        isBytesIndex = typesinternal.IsFunctionNamed(obj, "bytes", "Index")
        isStringsIndexByte = typesinternal.IsFunctionNamed(obj, "strings", "IndexByte")
        isBytesIndexByte = typesinternal.IsFunctionNamed(obj, "bytes", "IndexByte")
        )
        if !(isStringsIndex || isBytesIndex || isStringsIndexByte || isBytesIndexByte) {
        continue
        }
        Alan Donovan . resolved
        Rather than looking at every call in the package and testing (four times) whether it is one of the four callees we care about, it would be much more efficient to use the index to report just the handful of calls that we want:
        ```
        for _, obj := range []types.Object{
        index.Object("strings", "Index"),
        index.Object("strings", "IndexByte"),
        index.Object("bytes", "Index"),
        index.Object("bytes", "IndexByte"),
        } {
        // (obj may be nil)
        nextcall:
        for curCall := range index.Calls(obj) {
        ```
        Madeline Kalil

        Done

        Line 144, Patchset 7: selExpr, ok := indexCall.Fun.(*ast.SelectorExpr)
        Alan Donovan . resolved

        ast.Unparen(...).(T)

        Always call Unparen before any kind of type switch to remove irrelevant parens.

        But see next comment.

        Madeline Kalil

        Acknowledged

        Line 145, Patchset 7: if !ok {
        continue
        }
        Alan Donovan . resolved

        It is actually possible to call strings.Index without a selector, if the strings package is dot imported. In that case, Fun is only an `*ast.Ident` here. Fortunately we have the `typesinternal.UsedIdent(indexCall.Fun)` helper which will take care of these details and return the ident you care about.

        Madeline Kalil

        Done

        Line 159, Patchset 7: idxVar, ok := info.Defs[idx].(*types.Var)
        Alan Donovan . resolved

        Isn't this the same as idxObj? We already did the Defs lookup.

        Madeline Kalil

        Oops yes, thank you.

        Line 178, Patchset 7: afterPos := indexCall.Pos() // don't consider uses before the call to strings.Index etc.
        Alan Donovan . unresolved
        Strictly, we can't ignore uses before the call. Consider
        ```
        var index int
        f(&index)
        index = strings.Index(s, ...)
        g()
        if index >= 0 {
        use(s[:index])
        }
        ```
        In this example, f could escape a pointer to index (e.g. by storing it in a global) and then the call to g() could mutate index through that pointer. So any l-value reference to index other than its strings.Index assignment is potentially problematic.
        I think the simplest thing to do is to handle only cases where index is declared and assigned to strings.Index at the same time, for example:
        ```
        var index = strings.Index(...)
        ```
        or:
        ```
        index := strings.Index(...)
        ```
        but not:
        ```
        var index int
        index, zero := strings.Index(...), 0
        ```
        And the simplest way to detect that is for the logic at L129-143 to assert that the identifier is a def and skip otherwise:

        ```
        switch ek, i := curCall.ParentEdge(); ek {
        case edge.ValueSpec_Values:
        ...
        indexVar, _ = info.Defs[id].(*types.Var)
        }
        if indexVar == nil {
        continue
        }
        ```

        Then you don't need to worry about afterPos here, you can simply check all uses.

        Madeline Kalil

        Thanks, I didn't think of this case. There's logic in checkIdxUses that does not allow using any pointers of index like &index. I updated this function so that it doesn't only examine uses after "afterPos" and added a test case.

        Line 197, Patchset 7: var textEdits []analysis.TextEdit
        Alan Donovan . resolved

        edits

        Madeline Kalil

        Done

        Line 247, Patchset 7: Pos: indexCall.Pos(),
        Alan Donovan . resolved

        Rather than report the whole call, let's just flag the strings.Index portion, i.e. call.Fun.

        Madeline Kalil

        Done

        Line 249, Patchset 7: Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
        Alan Donovan . resolved

        Let's aim for "strings.Index call can be simplified using Cut"

        There's no need to repeat the package name.

        Madeline Kalil

        Done

        Line 252, Patchset 7: Message: fmt.Sprintf("%s can be replaced with %s", replace, with),
        Alan Donovan . resolved

        This should be a verb phrase describing the fix, e.g. "Simplify %s call using %s".

        Madeline Kalil

        Done

        Line 377, Patchset 7:// checkValue reports whether the check establishes that idx < 0 or idx > -1
        Alan Donovan . unresolved

        Helper functions tailored for stringscut should use names that make sense in the wider context of the modernize package. (Go has no file-local func declarations, even though that would sometimes be useful.)

        Madeline Kalil

        How about checkIdxComparison to match checkIdxUses?

        Line 380, Patchset 7:// inv: a check passed to checkValue should have idx as one of its operands.
        Alan Donovan . resolved

        has

        Madeline Kalil

        Done

        Line 385, Patchset 7: check.Op == token.GTR && isZeroIntConst(info, check.X) || (check.Op == token.EQL &&
        Alan Donovan . resolved

        linebreak

        Madeline Kalil

        Done

        Line 391, Patchset 7: if check.Op == token.GEQ && isZeroIntConst(info, check.Y) || check.Op == token.LEQ && isZeroIntConst(info, check.X) ||
        Alan Donovan . resolved

        linebreak

        Madeline Kalil

        Done

        Line 392, Patchset 7: check.Op == token.EQL && isZeroIntConst(info, check.X) || isZeroIntConst(info, check.Y) && check.Op == token.EQL {
        Alan Donovan . resolved

        linebreak

        Madeline Kalil

        Done

        Line 392, Patchset 7: check.Op == token.EQL && isZeroIntConst(info, check.X) || isZeroIntConst(info, check.Y) && check.Op == token.EQL {
        Alan Donovan . resolved

        flip the operands (...Op... && ...Y...) for consistency.

        Madeline Kalil

        Done

        Line 401, Patchset 7: switch e := expr.(type) {
        Alan Donovan . resolved

        return isIntConst(info, e, -1)

        Rename to isMinusOneIntConst to follow the pattern. Alternatively declare it locally within checkValue.

        Madeline Kalil

        I decided to just inline the call to isIntLiteral(info, e, -1)

        Line 414, Patchset 7: return slice.Low == nil || isZeroIntConst(info, slice.Low)
        Alan Donovan . unresolved

        Don't forget to check that the slice.Max operand (`s[::max]`) is also absent.

        Madeline Kalil

        Thanks, I check this inside checkIdxUses before the call to isBeforeSlice

        Open in Gerrit

        Related details

        Attention set is empty
        Submit Requirements:
        • requirement is not satisfiedCode-Review
        • requirement is not satisfiedNo-Unresolved-Comments
        • requirement is not satisfiedReview-Enforcement
        • requirement is not satisfiedTryBots-Pass
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: tools
        Gerrit-Branch: master
        Gerrit-Change-Id: Iff6c113587ba31664578baf1e757929d749363cb
        Gerrit-Change-Number: 706936
        Gerrit-PatchSet: 8
        Gerrit-Owner: Madeline Kalil <mka...@google.com>
        Gerrit-Reviewer: Alan Donovan <adon...@google.com>
        Gerrit-Reviewer: Madeline Kalil <mka...@google.com>
        Gerrit-CC: Ingo Oeser <night...@googlemail.com>
        Gerrit-CC: Mateusz Poliwczak <mpoliw...@gmail.com>
        Gerrit-Comment-Date: Wed, 15 Oct 2025 21:10:37 +0000
        Gerrit-HasComments: Yes
        Gerrit-Has-Labels: Yes
        Comment-In-Reply-To: Alan Donovan <adon...@google.com>
        unsatisfied_requirement
        open
        diffy
        Reply all
        Reply to author
        Forward
        0 new messages