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

View as plain text