Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscut.go

     1  // Copyright 2025 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modernize
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/constant"
    11  	"go/token"
    12  	"go/types"
    13  	"iter"
    14  	"strconv"
    15  
    16  	"golang.org/x/tools/go/analysis"
    17  	"golang.org/x/tools/go/analysis/passes/inspect"
    18  	"golang.org/x/tools/go/ast/edge"
    19  	"golang.org/x/tools/go/ast/inspector"
    20  	"golang.org/x/tools/go/types/typeutil"
    21  	"golang.org/x/tools/internal/analysis/analyzerutil"
    22  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    23  	"golang.org/x/tools/internal/astutil"
    24  	"golang.org/x/tools/internal/moreiters"
    25  	"golang.org/x/tools/internal/typesinternal"
    26  	"golang.org/x/tools/internal/typesinternal/typeindex"
    27  	"golang.org/x/tools/internal/versions"
    28  )
    29  
    30  var StringsCutAnalyzer = &analysis.Analyzer{
    31  	Name: "stringscut",
    32  	Doc:  analyzerutil.MustExtractDoc(doc, "stringscut"),
    33  	Requires: []*analysis.Analyzer{
    34  		inspect.Analyzer,
    35  		typeindexanalyzer.Analyzer,
    36  	},
    37  	Run: stringscut,
    38  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut",
    39  }
    40  
    41  // stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with
    42  // strings.{Cut,Contains}, and similar fixes for functions in the bytes package.
    43  // Consider some candidate for replacement i := strings.Index(s, substr).
    44  // The following must hold for a replacement to occur:
    45  //
    46  //  1. All instances of i and s must be in one of these forms.
    47  //
    48  //     Binary expressions must be inequalities equivalent to
    49  //     "Index failed" (e.g. i < 0) or "Index succeeded" (i >= 0),
    50  //     or identities such as these (and their negations):
    51  //
    52  //     0 > i                 (flips left and right)
    53  //     i <= -1, -1 >= i      (replace strict inequality by non-strict)
    54  //     i == -1, -1 == i      (Index() guarantees i < 0 => i == -1)
    55  //
    56  //     Slice expressions:
    57  //     a: s[:i], s[0:i]
    58  //     b: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr))
    59  //
    60  //  2. There can be no uses of s, substr, or i where they are
    61  //     potentially modified (i.e. in assignments, or function calls with unknown side
    62  //     effects).
    63  //
    64  // Then, the replacement involves the following substitutions:
    65  //
    66  //  1. Replace "i := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)"
    67  //
    68  //  2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok.
    69  //
    70  //  3. Replace slice expressions (a) with "before" and slice expressions (b) with after.
    71  //
    72  //  4. The assignments to before, after, and ok may use the blank identifier "_" if they are unused.
    73  //
    74  //     For example:
    75  //
    76  //     i := strings.Index(s, substr)
    77  //     if i >= 0 {
    78  //     use(s[:i], s[i+len(substr):])
    79  //     }
    80  //
    81  //     Would become:
    82  //
    83  //     before, after, ok := strings.Cut(s, substr)
    84  //     if ok {
    85  //     use(before, after)
    86  //     }
    87  //
    88  // If the condition involving `i` is equivalent to i >= 0, then we replace it with
    89  // `if ok`.
    90  // If the condition is negated (e.g. equivalent to `i < 0`), we use `if !ok` instead.
    91  // If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above,
    92  // then we replace them with before and after.
    93  //
    94  // When the index `i` is used only to check for the presence of the substring or byte slice,
    95  // the suggested fix uses Contains() instead of Cut.
    96  //
    97  // For example:
    98  //
    99  //	i := strings.Index(s, substr)
   100  //	if i >= 0 {
   101  //		return
   102  //	}
   103  //
   104  // Would become:
   105  //
   106  //	found := strings.Contains(s, substr)
   107  //	if found {
   108  //		return
   109  //	}
   110  func stringscut(pass *analysis.Pass) (any, error) {
   111  	var (
   112  		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
   113  		info  = pass.TypesInfo
   114  
   115  		stringsIndex     = index.Object("strings", "Index")
   116  		stringsIndexByte = index.Object("strings", "IndexByte")
   117  		bytesIndex       = index.Object("bytes", "Index")
   118  		bytesIndexByte   = index.Object("bytes", "IndexByte")
   119  	)
   120  
   121  	stringsplitCut(pass, index)
   122  
   123  	scopeFixCount := make(map[*types.Scope]int) // the number of times we have offered a fix within a given scope in the current pass
   124  
   125  	for _, obj := range []types.Object{
   126  		stringsIndex,
   127  		stringsIndexByte,
   128  		bytesIndex,
   129  		bytesIndexByte,
   130  	} {
   131  		// (obj may be nil)
   132  	nextcall:
   133  		for curCall := range index.Calls(obj) {
   134  			// Check file version.
   135  			if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
   136  				continue // strings.Index not available in this file
   137  			}
   138  			indexCall := curCall.Node().(*ast.CallExpr) // the call to strings.Index, etc.
   139  			obj := typeutil.Callee(info, indexCall)
   140  			if obj == nil {
   141  				continue
   142  			}
   143  
   144  			var iIdent *ast.Ident // defining identifier of i var
   145  			switch ek, idx := curCall.ParentEdge(); ek {
   146  			case edge.ValueSpec_Values:
   147  				// Have: var i = strings.Index(...)
   148  				// If the call occurs in a multi-value declaration or assignment, don't suggest a fix because it would produce invalid code (See golang/go#78643).
   149  				spec := curCall.Parent().Node().(*ast.ValueSpec)
   150  				if len(spec.Names) != 1 {
   151  					continue
   152  				}
   153  				curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
   154  				iIdent = curName.Node().(*ast.Ident)
   155  			case edge.AssignStmt_Rhs:
   156  				// Have: i := strings.Index(...)
   157  				// (Must be i's definition.)
   158  				assign := curCall.Parent().Node().(*ast.AssignStmt)
   159  				if len(assign.Lhs) != 1 {
   160  					continue
   161  				}
   162  				curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
   163  				iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil
   164  			}
   165  
   166  			if iIdent == nil {
   167  				continue
   168  			}
   169  			// Inv: iIdent is i's definition. The following would be skipped: 'var i int; i = strings.Index(...)'
   170  			// Get uses of i.
   171  			iObj := info.ObjectOf(iIdent)
   172  			if iObj == nil {
   173  				continue
   174  			}
   175  
   176  			var (
   177  				s      = indexCall.Args[0]
   178  				substr = indexCall.Args[1]
   179  			)
   180  
   181  			// Check that there are no statements that alter the value of s
   182  			// or substr after the call to Index().
   183  			if !indexArgValid(info, index, s, indexCall.Pos()) ||
   184  				!indexArgValid(info, index, substr, indexCall.Pos()) {
   185  				continue nextcall
   186  			}
   187  
   188  			// Next, examine all uses of i. If the only uses are of the
   189  			// forms mentioned above (e.g. i < 0, i >= 0, s[:i] and s[i +
   190  			// len(substr)]), then we can replace the call to Index()
   191  			// with a call to Cut() and use the returned ok, before,
   192  			// and after variables accordingly.
   193  			negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr, iObj)
   194  
   195  			// Either there are no uses of before, after, or ok, or some use
   196  			// of i does not match our criteria - don't suggest a fix.
   197  			if negative == nil && nonnegative == nil && beforeSlice == nil && afterSlice == nil {
   198  				continue
   199  			}
   200  
   201  			// If the only uses are ok and !ok, don't suggest a Cut() fix - these should be using Contains()
   202  			isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0
   203  
   204  			enclosingBlock, ok := moreiters.First(curCall.Enclosing((*ast.BlockStmt)(nil)))
   205  			if !ok {
   206  				continue
   207  			}
   208  			scope := iObj.Parent()
   209  			// Generate fresh names for ok, before, after, found, but only if
   210  			// they are defined by the end of the enclosing block and used
   211  			// within the enclosing block after the Index call. We need a Cursor
   212  			// for the end of the enclosing block, but we can't just find the
   213  			// Cursor at scope.End() because it corresponds to the entire
   214  			// enclosingBlock. Instead, get the last child of the enclosing
   215  			// block.
   216  			lastStmtCur, _ := enclosingBlock.LastChild()
   217  			lastStmt := lastStmtCur.Node()
   218  
   219  			fresh := func(preferred string) string {
   220  				return freshName(info, index, scope, lastStmt.End(), lastStmtCur, enclosingBlock, iIdent.Pos(), preferred)
   221  			}
   222  
   223  			var okVarName, beforeVarName, afterVarName, foundVarName string
   224  			if isContains {
   225  				foundVarName = fresh("found")
   226  			} else {
   227  				okVarName = fresh("ok")
   228  				beforeVarName = fresh("before")
   229  				afterVarName = fresh("after")
   230  			}
   231  
   232  			// If we are already suggesting a fix within the index's scope, we
   233  			// must get fresh names for before, after and ok.
   234  			// This is a specific symptom of the general problem that analyzers
   235  			// can generate conflicting fixes.
   236  			if scopeFixCount[scope] > 0 {
   237  				suffix := scopeFixCount[scope] - 1 // start at 0
   238  				if isContains {
   239  					foundVarName = fresh(fmt.Sprintf("%s%d", foundVarName, suffix))
   240  				} else {
   241  					okVarName = fresh(fmt.Sprintf("%s%d", okVarName, suffix))
   242  					beforeVarName = fresh(fmt.Sprintf("%s%d", beforeVarName, suffix))
   243  					afterVarName = fresh(fmt.Sprintf("%s%d", afterVarName, suffix))
   244  				}
   245  			}
   246  
   247  			// If there will be no uses of ok, before, or after, use the
   248  			// blank identifier instead.
   249  			if len(negative) == 0 && len(nonnegative) == 0 {
   250  				okVarName = "_"
   251  			}
   252  			if len(beforeSlice) == 0 {
   253  				beforeVarName = "_"
   254  			}
   255  			if len(afterSlice) == 0 {
   256  				afterVarName = "_"
   257  			}
   258  
   259  			var edits []analysis.TextEdit
   260  			replace := func(exprs []ast.Expr, new string) {
   261  				for _, expr := range exprs {
   262  					edits = append(edits, analysis.TextEdit{
   263  						Pos:     expr.Pos(),
   264  						End:     expr.End(),
   265  						NewText: []byte(new),
   266  					})
   267  				}
   268  			}
   269  			// Get the ident for the call to strings.Index, which could just be
   270  			// "Index" if the strings package is dot imported.
   271  			indexCallId := typesinternal.UsedIdent(info, indexCall.Fun)
   272  			replacedFunc := "Cut"
   273  			if isContains {
   274  				replacedFunc = "Contains"
   275  				replace(negative, "!"+foundVarName) // idx < 0   ->  !found
   276  				replace(nonnegative, foundVarName)  // idx > -1  ->   found
   277  
   278  				// Replace the assignment with found, and replace the call to
   279  				// Index or IndexByte with a call to Contains.
   280  				// i     := strings.Index   (...)
   281  				// -----            --------
   282  				// found := strings.Contains(...)
   283  				edits = append(edits, analysis.TextEdit{
   284  					Pos:     iIdent.Pos(),
   285  					End:     iIdent.End(),
   286  					NewText: []byte(foundVarName),
   287  				}, analysis.TextEdit{
   288  					Pos:     indexCallId.Pos(),
   289  					End:     indexCallId.End(),
   290  					NewText: []byte("Contains"),
   291  				})
   292  			} else {
   293  				replace(negative, "!"+okVarName)    // idx < 0   ->  !ok
   294  				replace(nonnegative, okVarName)     // idx > -1  ->   ok
   295  				replace(beforeSlice, beforeVarName) // s[:idx]   ->   before
   296  				replace(afterSlice, afterVarName)   // s[idx+k:] ->   after
   297  
   298  				// Replace the assignment with before, after, ok, and replace
   299  				// the call to Index or IndexByte with a call to Cut.
   300  				// i     			 := strings.Index(...)
   301  				// -----------------            -----
   302  				// before, after, ok := strings.Cut  (...)
   303  				edits = append(edits, analysis.TextEdit{
   304  					Pos:     iIdent.Pos(),
   305  					End:     iIdent.End(),
   306  					NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName),
   307  				}, analysis.TextEdit{
   308  					Pos:     indexCallId.Pos(),
   309  					End:     indexCallId.End(),
   310  					NewText: []byte("Cut"),
   311  				})
   312  			}
   313  
   314  			// Calls to IndexByte have a byte as their second arg, which
   315  			// must be converted to a string or []byte to be a valid arg for Cut/Contains.
   316  			if obj.Name() == "IndexByte" {
   317  				switch obj.Pkg().Name() {
   318  				case "strings":
   319  					searchByteVal := info.Types[substr].Value
   320  					if searchByteVal == nil {
   321  						// substr is a variable, e.g. substr := byte('b')
   322  						// use string(substr)
   323  						edits = append(edits, []analysis.TextEdit{
   324  							{
   325  								Pos:     substr.Pos(),
   326  								NewText: []byte("string("),
   327  							},
   328  							{
   329  								Pos:     substr.End(),
   330  								NewText: []byte(")"),
   331  							},
   332  						}...)
   333  					} else {
   334  						// substr is a byte constant
   335  						val, _ := constant.Int64Val(searchByteVal) // inv: must be a valid byte
   336  						// strings.Cut/Contains requires a string, so convert byte literal to string literal; e.g. 'a' -> "a", 55 -> "7"
   337  						edits = append(edits, analysis.TextEdit{
   338  							Pos:     substr.Pos(),
   339  							End:     substr.End(),
   340  							NewText: strconv.AppendQuote(nil, string(byte(val))),
   341  						})
   342  					}
   343  				case "bytes":
   344  					// bytes.Cut/Contains requires a []byte, so wrap substr in a []byte{}
   345  					edits = append(edits, []analysis.TextEdit{
   346  						{
   347  							Pos:     substr.Pos(),
   348  							NewText: []byte("[]byte{"),
   349  						},
   350  						{
   351  							Pos:     substr.End(),
   352  							NewText: []byte("}"),
   353  						},
   354  					}...)
   355  				}
   356  			}
   357  			scopeFixCount[scope]++
   358  			pass.Report(analysis.Diagnostic{
   359  				Pos: indexCall.Fun.Pos(),
   360  				End: indexCall.Fun.End(),
   361  				Message: fmt.Sprintf("%s.%s can be simplified using %s.%s",
   362  					obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
   363  				Category: "stringscut",
   364  				SuggestedFixes: []analysis.SuggestedFix{{
   365  					Message:   fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
   366  					TextEdits: edits,
   367  				}},
   368  			})
   369  		}
   370  	}
   371  
   372  	return nil, nil
   373  }
   374  
   375  // stringsplitCut reports patterns where strings.Split or strings.SplitN with
   376  // n=2 is immediately indexed at [0], which can be simplified to strings.Cut,
   377  // when sep is a non-empty string constant. The transformation is
   378  // semantics-preserving only for non-empty sep: strings.Split(s, "")[0]
   379  // returns the first character of s, but strings.Cut(s, "").before is "".
   380  // For variable sep the value is unknown at analysis time, so we conservatively
   381  // skip those cases too.
   382  //
   383  // For example:
   384  //
   385  //	x := strings.SplitN(s, ",", 2)[0]
   386  //	              ------              --
   387  //	x, _, _ := strings.Cut(s, ",")
   388  //
   389  // Requires Go 1.18 (when strings.Cut was added).
   390  func stringsplitCut(pass *analysis.Pass, index *typeindex.Index) {
   391  	info := pass.TypesInfo
   392  
   393  	stringsSplit := index.Object("strings", "Split")
   394  	stringsSplitN := index.Object("strings", "SplitN")
   395  
   396  	for _, obj := range []types.Object{stringsSplit, stringsSplitN} {
   397  		for curCall := range index.Calls(obj) {
   398  			callExpr := curCall.Node().(*ast.CallExpr)
   399  
   400  			// For SplitN, the third argument must be the integer constant 2.
   401  			if obj.Name() == "SplitN" && !isIntLiteral(info, callExpr.Args[2], 2) {
   402  				continue
   403  			}
   404  
   405  			// Sep must be a non-empty constant string.
   406  			// strings.Split(s, "")[0] returns the first character of s, but
   407  			// strings.Cut(s, "").before is "", so the semantics differ for
   408  			// an empty sep. For a variable sep we cannot rule out "" at
   409  			// analysis time, so we conservatively skip those cases too.
   410  			sepTV := info.Types[callExpr.Args[1]]
   411  			if sepTV.Value == nil || constant.StringVal(sepTV.Value) == "" {
   412  				continue
   413  			}
   414  
   415  			// The call must be the X of an IndexExpr.
   416  			if curCall.ParentEdgeKind() != edge.IndexExpr_X {
   417  				continue
   418  			}
   419  			parent := curCall.Parent()
   420  			indexExpr := parent.Node().(*ast.IndexExpr)
   421  
   422  			// The index must be the integer constant 0.
   423  			if !isZeroIntConst(info, indexExpr.Index) {
   424  				continue
   425  			}
   426  
   427  			// The IndexExpr must be the sole RHS of an assignment statement.
   428  			if parent.ParentEdgeKind() != edge.AssignStmt_Rhs {
   429  				continue
   430  			}
   431  			assign := parent.Parent().Node().(*ast.AssignStmt)
   432  			if assign.Tok != token.DEFINE || len(assign.Lhs) != 1 {
   433  				continue
   434  			}
   435  
   436  			// The LHS must be a single non-blank identifier.
   437  			lhsIdent, ok := assign.Lhs[0].(*ast.Ident)
   438  			if !ok || lhsIdent.Name == "_" {
   439  				continue
   440  			}
   441  
   442  			// strings.Cut requires Go 1.18.
   443  			if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
   444  				continue
   445  			}
   446  
   447  			// Build the fix.
   448  			//
   449  			//  x  := strings.SplitN(s, sep, 2)[0]
   450  			//  ---           ------             ---
   451  			//  x, _, _ := strings.Cut(s, sep)
   452  			callFunIdent := typesinternal.UsedIdent(info, callExpr.Fun)
   453  
   454  			var edits []analysis.TextEdit
   455  
   456  			// LHS: insert ", _, _" after x
   457  			edits = append(edits, analysis.TextEdit{
   458  				Pos:     lhsIdent.End(),
   459  				End:     lhsIdent.End(),
   460  				NewText: []byte(", _, _"),
   461  			})
   462  
   463  			// Function name: Split/SplitN → Cut
   464  			edits = append(edits, analysis.TextEdit{
   465  				Pos:     callFunIdent.Pos(),
   466  				End:     callFunIdent.End(),
   467  				NewText: []byte("Cut"),
   468  			})
   469  
   470  			// For SplitN: remove the ", 2" third argument.
   471  			if obj.Name() == "SplitN" {
   472  				edits = append(edits, analysis.TextEdit{
   473  					Pos: callExpr.Args[1].End(), // after sep
   474  					End: callExpr.Rparen,        // before )
   475  				})
   476  			}
   477  
   478  			// Remove the "[0]" index expression.
   479  			edits = append(edits, analysis.TextEdit{
   480  				Pos: indexExpr.Lbrack,
   481  				End: indexExpr.End(),
   482  			})
   483  
   484  			pass.Report(analysis.Diagnostic{
   485  				Pos:      callExpr.Fun.Pos(),
   486  				End:      callExpr.Fun.End(),
   487  				Message:  fmt.Sprintf("strings.%s call can be simplified using strings.Cut", obj.Name()),
   488  				Category: "stringscut",
   489  				SuggestedFixes: []analysis.SuggestedFix{{
   490  					Message:   fmt.Sprintf("Simplify strings.%s call using strings.Cut", obj.Name()),
   491  					TextEdits: edits,
   492  				}},
   493  			})
   494  		}
   495  	}
   496  }
   497  
   498  // indexArgValid reports whether expr is a valid strings.Index(_, _) arg
   499  // for the transformation. An arg is valid iff it is:
   500  // - constant;
   501  // - a local variable with no modifying uses after the Index() call; or
   502  // - []byte(x) where x is also valid by this definition.
   503  // All other expressions are assumed not referentially transparent,
   504  // so we cannot be sure that all uses are safe to replace.
   505  func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool {
   506  	tv := info.Types[expr]
   507  	if tv.Value != nil {
   508  		return true // constant
   509  	}
   510  	switch expr := expr.(type) {
   511  	case *ast.CallExpr:
   512  		return types.Identical(tv.Type, byteSliceType) &&
   513  			info.Types[expr.Fun].IsType() && // make sure this isn't a function that returns a byte slice
   514  			indexArgValid(info, index, expr.Args[0], afterPos) // check s in []byte(s)
   515  	case *ast.Ident:
   516  		for use := range index.Uses(info.Uses[expr]) {
   517  			if typesinternal.IsAssignedOrAddressTaken(info, use) {
   518  				return false
   519  			}
   520  		}
   521  		return true
   522  	default:
   523  		// For now, skip instances where s or substr are not
   524  		// identifiers, basic lits, or call expressions of the form
   525  		// []byte(s).
   526  		// TODO(mkalil): Handle s and substr being expressions like ptr.field[i].
   527  		// From adonovan: We'd need to analyze s and substr to see
   528  		// whether they are referentially transparent, and if not,
   529  		// analyze all code between declaration and use and see if
   530  		// there are statements or expressions with potential side
   531  		// effects.
   532  		return false
   533  	}
   534  }
   535  
   536  // checkIdxUses inspects the uses of i to make sure they match certain criteria that
   537  // allows us to suggest a modernization. If all uses of i, s and substr match
   538  // one of the following four valid formats, it returns a list of occurrences for
   539  // each format. If any of the uses do not match one of the formats, return nil
   540  // for all values, since we should not offer a replacement.
   541  // 1. negative - a condition equivalent to i < 0
   542  // 2. nonnegative - a condition equivalent to i >= 0
   543  // 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i]
   544  // 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr))
   545  //
   546  // Additionally, all beforeSlice and afterSlice uses must be dominated by a
   547  // nonnegative guard on i (i.e., inside the body of an if whose condition
   548  // checks i >= 0, or in the else of a negative check, or after an
   549  // early-return negative check). This ensures that the rewrite from
   550  // s[i+len(sep):] to "after" preserves semantics, since when i == -1,
   551  // s[i+len(sep):] may yield a valid substring (e.g. s[0:] for single-byte
   552  // separators), but "after" would be "".
   553  //
   554  // When len(substr)==1, it's safe to use s[i+1:] even when i < 0.
   555  // Otherwise, each replacement of s[i+1:] must be guarded by a check
   556  // that i is nonnegative.
   557  func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr, iObj types.Object) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
   558  	requireGuard := true
   559  	if l := constSubstrLen(info, substr); l != -1 && l != 1 {
   560  		requireGuard = false
   561  	}
   562  
   563  	use := func(cur inspector.Cursor) bool {
   564  		ek := cur.ParentEdgeKind()
   565  		n := cur.Parent().Node()
   566  		switch ek {
   567  		case edge.BinaryExpr_X, edge.BinaryExpr_Y:
   568  			check := n.(*ast.BinaryExpr)
   569  			switch checkIdxComparison(info, check, iObj) {
   570  			case -1:
   571  				negative = append(negative, check)
   572  				return true
   573  			case 1:
   574  				nonnegative = append(nonnegative, check)
   575  				return true
   576  			}
   577  			// Check is not equivalent to that i < 0 or i >= 0.
   578  			// Might be part of an outer slice expression like s[i + k]
   579  			// which requires a different check.
   580  			// Check that the thing being sliced is s and that the slice
   581  			// doesn't have a max index.
   582  			if slice, ok := cur.Parent().Parent().Node().(*ast.SliceExpr); ok &&
   583  				sameObject(info, s, slice.X) &&
   584  				slice.Max == nil {
   585  				if isBeforeSlice(info, ek, slice) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) {
   586  					beforeSlice = append(beforeSlice, slice)
   587  					return true
   588  				} else if isAfterSlice(info, ek, slice, substr) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) {
   589  					afterSlice = append(afterSlice, slice)
   590  					return true
   591  				}
   592  			}
   593  		case edge.SliceExpr_Low, edge.SliceExpr_High:
   594  			slice := n.(*ast.SliceExpr)
   595  			// Check that the thing being sliced is s and that the slice doesn't
   596  			// have a max index.
   597  			if sameObject(info, s, slice.X) && slice.Max == nil {
   598  				if isBeforeSlice(info, ek, slice) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) {
   599  					beforeSlice = append(beforeSlice, slice)
   600  					return true
   601  				} else if isAfterSlice(info, ek, slice, substr) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) {
   602  					afterSlice = append(afterSlice, slice)
   603  					return true
   604  				}
   605  			}
   606  		}
   607  		return false
   608  	}
   609  
   610  	for curIdent := range uses {
   611  		if !use(curIdent) {
   612  			return nil, nil, nil, nil
   613  		}
   614  	}
   615  	return negative, nonnegative, beforeSlice, afterSlice
   616  }
   617  
   618  // checkIdxComparison reports whether the check is equivalent to i < 0 or its negation, or neither.
   619  // For equivalent to i >= 0, we only accept this exact BinaryExpr since
   620  // expressions like i > 0 or i >= 1 make a stronger statement about the value of i.
   621  // We avoid suggesting a fix in this case since it may result in an invalid
   622  // transformation (See golang/go#76687).
   623  // Since strings.Index returns exactly -1 if the substring is not found, we
   624  // don't need to handle expressions like i <= -3.
   625  // We return 0 if the expression does not match any of these options.
   626  func checkIdxComparison(info *types.Info, check *ast.BinaryExpr, iObj types.Object) int {
   627  	isI := func(e ast.Expr) bool {
   628  		id, ok := e.(*ast.Ident)
   629  		return ok && info.Uses[id] == iObj
   630  	}
   631  	if !isI(check.X) && !isI(check.Y) {
   632  		return 0
   633  	}
   634  
   635  	// Ensure that the constant (if any) is on the right.
   636  	x, op, y := check.X, check.Op, check.Y
   637  	if info.Types[x].Value != nil {
   638  		x, op, y = y, flip(op), x
   639  	}
   640  
   641  	yIsInt := func(k int64) bool {
   642  		return isIntLiteral(info, y, k)
   643  	}
   644  
   645  	if op == token.LSS && yIsInt(0) || // i < 0
   646  		op == token.EQL && yIsInt(-1) || // i == -1
   647  		op == token.LEQ && yIsInt(-1) { // i <= -1
   648  		return -1 // check <=> i is negative
   649  	}
   650  
   651  	if op == token.GEQ && yIsInt(0) || // i >= 0
   652  		op == token.NEQ && yIsInt(-1) || // i != -1
   653  		op == token.GTR && yIsInt(-1) { // i > -1
   654  		return +1 // check <=> i is non-negative
   655  	}
   656  
   657  	return 0 // unknown
   658  }
   659  
   660  // flip changes the comparison token as if the operands were flipped.
   661  // It is defined only for == and the four inequalities.
   662  func flip(op token.Token) token.Token {
   663  	switch op {
   664  	case token.EQL:
   665  		return token.EQL // (same)
   666  	case token.GEQ:
   667  		return token.LEQ
   668  	case token.GTR:
   669  		return token.LSS
   670  	case token.LEQ:
   671  		return token.GEQ
   672  	case token.LSS:
   673  		return token.GTR
   674  	}
   675  	return op
   676  }
   677  
   678  // isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i].
   679  func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool {
   680  	return ek == edge.SliceExpr_High && (slice.Low == nil || isZeroIntConst(info, slice.Low))
   681  }
   682  
   683  // constSubstrLen returns the constant length of substr, or -1 if unknown.
   684  func constSubstrLen(info *types.Info, substr ast.Expr) int {
   685  	// Handle len([]byte(substr))
   686  	if call, ok := substr.(*ast.CallExpr); ok {
   687  		tv := info.Types[call.Fun]
   688  		if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
   689  			// Only one arg in []byte conversion.
   690  			substr = call.Args[0]
   691  		}
   692  	}
   693  	substrVal := info.Types[substr].Value
   694  	if substrVal != nil {
   695  		switch substrVal.Kind() {
   696  		case constant.String:
   697  			return len(constant.StringVal(substrVal))
   698  		case constant.Int:
   699  			// constant.Value is a byte literal, e.g. bytes.IndexByte(_, 'a')
   700  			// or a numeric byte literal, e.g. bytes.IndexByte(_, 65)
   701  			// ([]byte(rune) is not legal.)
   702  			return 1
   703  		}
   704  	}
   705  	return -1
   706  }
   707  
   708  // isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):],
   709  // or s[i + k:] where k is a const is equal to len(substr).
   710  func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr ast.Expr) bool {
   711  	lowExpr, ok := slice.Low.(*ast.BinaryExpr)
   712  	if !ok || slice.High != nil {
   713  		return false
   714  	}
   715  	// Returns true if the expression is a call to len(substr).
   716  	isLenCall := func(expr ast.Expr) bool {
   717  		call, ok := expr.(*ast.CallExpr)
   718  		if !ok || len(call.Args) != 1 {
   719  			return false
   720  		}
   721  		return sameObject(info, substr, call.Args[0]) && typeutil.Callee(info, call) == builtinLen
   722  	}
   723  
   724  	substrLen := constSubstrLen(info, substr)
   725  
   726  	switch ek {
   727  	case edge.BinaryExpr_X:
   728  		kVal := info.Types[lowExpr.Y].Value
   729  		if kVal == nil {
   730  			// i + len(substr)
   731  			return lowExpr.Op == token.ADD && isLenCall(lowExpr.Y)
   732  		} else {
   733  			// i + k
   734  			kInt, ok := constant.Int64Val(kVal)
   735  			return ok && substrLen == int(kInt)
   736  		}
   737  	case edge.BinaryExpr_Y:
   738  		kVal := info.Types[lowExpr.X].Value
   739  		if kVal == nil {
   740  			// len(substr) + i
   741  			return lowExpr.Op == token.ADD && isLenCall(lowExpr.X)
   742  		} else {
   743  			// k + i
   744  			kInt, ok := constant.Int64Val(kVal)
   745  			return ok && substrLen == int(kInt)
   746  		}
   747  	}
   748  	return false
   749  }
   750  
   751  // isSliceIndexGuarded reports whether a use of the index variable i (at the given cursor)
   752  // inside a slice expression is dominated by a nonnegative guard.
   753  // A use is considered guarded if any of the following are true:
   754  //   - It is inside the Body of an IfStmt whose condition is a nonnegative check on i.
   755  //   - It is inside the Else of an IfStmt whose condition is a negative check on i.
   756  //   - It is preceded (in the same block) by an IfStmt whose condition is a
   757  //     negative check on i with a terminating body (e.g., early return).
   758  //
   759  // Conversely, a use is immediately rejected if:
   760  //   - It is inside the Body of an IfStmt whose condition is a negative check on i.
   761  //   - It is inside the Else of an IfStmt whose condition is a nonnegative check on i.
   762  //
   763  // We have already checked (see [hasModifyingUses]) that there are no
   764  // intervening uses (incl. via aliases) of i that might alter its value.
   765  func isSliceIndexGuarded(info *types.Info, cur inspector.Cursor, iObj types.Object) bool {
   766  	for anc := range cur.Enclosing() {
   767  		switch anc.ParentEdgeKind() {
   768  		case edge.IfStmt_Body, edge.IfStmt_Else:
   769  			ifStmt := anc.Parent().Node().(*ast.IfStmt)
   770  			check := condChecksIdx(info, ifStmt.Cond, iObj)
   771  			if anc.ParentEdgeKind() == edge.IfStmt_Else {
   772  				check = -check
   773  			}
   774  			if check > 0 {
   775  				return true // inside nonnegative-guarded block (i >= 0 here)
   776  			}
   777  			if check < 0 {
   778  				return false // inside negative-guarded block (i < 0 here)
   779  			}
   780  		case edge.BlockStmt_List:
   781  			// Check preceding siblings for early-return negative checks.
   782  			for sib, ok := anc.PrevSibling(); ok; sib, ok = sib.PrevSibling() {
   783  				ifStmt, ok := sib.Node().(*ast.IfStmt)
   784  				if ok && condChecksIdx(info, ifStmt.Cond, iObj) < 0 && bodyTerminates(ifStmt.Body) {
   785  					return true // preceded by early-return negative check
   786  				}
   787  			}
   788  		case edge.FuncDecl_Body, edge.FuncLit_Body:
   789  			return false // stop at function boundary
   790  		}
   791  	}
   792  	return false
   793  }
   794  
   795  // condChecksIdx reports whether cond is a BinaryExpr that checks
   796  // the index variable iObj for negativity or non-negativity.
   797  // Returns -1 for negative (e.g. i < 0), +1 for nonnegative (e.g. i >= 0), 0 otherwise.
   798  func condChecksIdx(info *types.Info, cond ast.Expr, iObj types.Object) int {
   799  	binExpr, ok := cond.(*ast.BinaryExpr)
   800  	if !ok {
   801  		return 0
   802  	}
   803  	return checkIdxComparison(info, binExpr, iObj)
   804  }
   805  
   806  // bodyTerminates reports whether the given block statement unconditionally
   807  // terminates execution (via return, break, continue, or goto).
   808  func bodyTerminates(block *ast.BlockStmt) bool {
   809  	if len(block.List) == 0 {
   810  		return false
   811  	}
   812  	last := block.List[len(block.List)-1]
   813  	switch last.(type) {
   814  	case *ast.ReturnStmt, *ast.BranchStmt:
   815  		return true // return, break, continue, goto
   816  	}
   817  	return false
   818  }
   819  
   820  // sameObject reports whether we know that the expressions resolve to the same object.
   821  func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool {
   822  	if ident1, ok := expr1.(*ast.Ident); ok {
   823  		if ident2, ok := expr2.(*ast.Ident); ok {
   824  			uses1, ok1 := info.Uses[ident1]
   825  			uses2, ok2 := info.Uses[ident2]
   826  			return ok1 && ok2 && uses1 == uses2
   827  		}
   828  	}
   829  	return false
   830  }
   831  

View as plain text