| // 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 ( |
| "fmt" |
| "go/ast" |
| "go/constant" |
| "go/token" |
| "go/types" |
| "iter" |
| "strconv" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/passes/inspect" |
| "golang.org/x/tools/go/ast/edge" |
| "golang.org/x/tools/go/ast/inspector" |
| "golang.org/x/tools/go/types/typeutil" |
| "golang.org/x/tools/internal/analysis/analyzerutil" |
| typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" |
| "golang.org/x/tools/internal/astutil" |
| "golang.org/x/tools/internal/goplsexport" |
| "golang.org/x/tools/internal/moreiters" |
| "golang.org/x/tools/internal/typesinternal" |
| "golang.org/x/tools/internal/typesinternal/typeindex" |
| "golang.org/x/tools/internal/versions" |
| ) |
| |
| var stringscutAnalyzer = &analysis.Analyzer{ |
| Name: "stringscut", |
| Doc: analyzerutil.MustExtractDoc(doc, "stringscut"), |
| Requires: []*analysis.Analyzer{ |
| inspect.Analyzer, |
| typeindexanalyzer.Analyzer, |
| }, |
| Run: stringscut, |
| URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut", |
| } |
| |
| func init() { |
| // Export to gopls until this is a published modernizer. |
| goplsexport.StringsCutModernizer = stringscutAnalyzer |
| } |
| |
| // stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with |
| // strings.{Cut,Contains}, and similar fixes for functions in the bytes package. |
| // Consider some candidate for replacement i := strings.Index(s, substr). |
| // The following must hold for a replacement to occur: |
| // |
| // 1. All instances of i and s must be in one of these forms. |
| // |
| // Binary expressions must be inequalities equivalent to |
| // "Index failed" (e.g. i < 0) or "Index succeeded" (i >= 0), |
| // or identities such as these (and their negations): |
| // |
| // 0 > i (flips left and right) |
| // i <= -1, -1 >= i (replace strict inequality by non-strict) |
| // i == -1, -1 == i (Index() guarantees i < 0 => i == -1) |
| // |
| // Slice expressions: |
| // a: s[:i], s[0:i] |
| // b: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr)) |
| // |
| // 2. There can be no uses of s, substr, or i 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 "i := 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. |
| // |
| // For example: |
| // |
| // i := strings.Index(s, substr) |
| // if i >= 0 { |
| // use(s[:i], s[i+len(substr):]) |
| // } |
| // |
| // Would become: |
| // |
| // before, after, ok := strings.Cut(s, substr) |
| // if ok { |
| // use(before, after) |
| // } |
| // |
| // If the condition involving `i` is equivalent to i >= 0, then we replace it with |
| // `if ok“. |
| // If the condition is negated (e.g. equivalent to `i < 0`), we use `if !ok` instead. |
| // If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above, |
| // then we replace them with before and after. |
| // |
| // When the index `i` is used only to check for the presence of the substring or byte slice, |
| // the suggested fix uses Contains() instead of Cut. |
| // |
| // For example: |
| // |
| // i := strings.Index(s, substr) |
| // if i >= 0 { |
| // return |
| // } |
| // |
| // Would become: |
| // |
| // found := strings.Contains(s, substr) |
| // if found { |
| // return |
| // } |
| func stringscut(pass *analysis.Pass) (any, error) { |
| var ( |
| 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") |
| ) |
| |
| scopeFixCount := make(map[*types.Scope]int) // the number of times we have offered a fix within a given scope in the current pass |
| |
| for _, obj := range []types.Object{ |
| stringsIndex, |
| stringsIndexByte, |
| bytesIndex, |
| bytesIndexByte, |
| } { |
| // (obj may be nil) |
| nextcall: |
| for curCall := range index.Calls(obj) { |
| // Check file version. |
| if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) { |
| continue // strings.Index not available in this file |
| } |
| indexCall := curCall.Node().(*ast.CallExpr) // the call to strings.Index, etc. |
| obj := typeutil.Callee(info, indexCall) |
| if obj == nil { |
| continue |
| } |
| |
| var iIdent *ast.Ident // defining identifier of i var |
| switch ek, idx := curCall.ParentEdge(); ek { |
| case edge.ValueSpec_Values: |
| // Have: var i = strings.Index(...) |
| // 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). |
| spec := curCall.Parent().Node().(*ast.ValueSpec) |
| if len(spec.Names) != 1 { |
| continue |
| } |
| 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.) |
| assign := curCall.Parent().Node().(*ast.AssignStmt) |
| if len(assign.Lhs) != 1 { |
| continue |
| } |
| curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx) |
| iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil |
| } |
| |
| if iIdent == nil { |
| continue |
| } |
| // Inv: iIdent is i's definition. The following would be skipped: 'var i int; i = strings.Index(...)' |
| // Get uses of i. |
| iObj := info.ObjectOf(iIdent) |
| if iObj == nil { |
| continue |
| } |
| |
| var ( |
| s = indexCall.Args[0] |
| substr = indexCall.Args[1] |
| ) |
| |
| // Check that there are no statements that alter the value of s |
| // or substr after the call to Index(). |
| if !indexArgValid(info, index, s, indexCall.Pos()) || |
| !indexArgValid(info, index, substr, indexCall.Pos()) { |
| continue nextcall |
| } |
| |
| // Next, examine all uses of i. If the only uses are of the |
| // forms mentioned above (e.g. i < 0, i >= 0, s[:i] and s[i + |
| // len(substr)]), then we can replace the call to Index() |
| // with a call to Cut() and use the returned ok, before, |
| // and after variables accordingly. |
| negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr, iObj) |
| |
| // Either there are no uses of before, after, or ok, or some use |
| // of i does not match our criteria - don't suggest a fix. |
| if negative == nil && nonnegative == nil && beforeSlice == nil && afterSlice == nil { |
| continue |
| } |
| |
| // If the only uses are ok and !ok, don't suggest a Cut() fix - these should be using Contains() |
| isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0 |
| |
| enclosingBlock, ok := moreiters.First(curCall.Enclosing((*ast.BlockStmt)(nil))) |
| if !ok { |
| continue |
| } |
| scope := iObj.Parent() |
| // Generate fresh names for ok, before, after, found, but only if |
| // they are defined by the end of the enclosing block and used |
| // within the enclosing block after the Index call. We need a Cursor |
| // for the end of the enclosing block, but we can't just find the |
| // Cursor at scope.End() because it corresponds to the entire |
| // enclosingBlock. Instead, get the last child of the enclosing |
| // block. |
| lastStmtCur, _ := enclosingBlock.LastChild() |
| lastStmt := lastStmtCur.Node() |
| |
| fresh := func(preferred string) string { |
| return freshName(info, index, scope, lastStmt.End(), lastStmtCur, enclosingBlock, iIdent.Pos(), preferred) |
| } |
| |
| var okVarName, beforeVarName, afterVarName, foundVarName string |
| if isContains { |
| foundVarName = fresh("found") |
| } else { |
| okVarName = fresh("ok") |
| beforeVarName = fresh("before") |
| afterVarName = fresh("after") |
| } |
| |
| // If we are already suggesting a fix within the index's scope, we |
| // must get fresh names for before, after and ok. |
| // This is a specific symptom of the general problem that analyzers |
| // can generate conflicting fixes. |
| if scopeFixCount[scope] > 0 { |
| suffix := scopeFixCount[scope] - 1 // start at 0 |
| if isContains { |
| foundVarName = fresh(fmt.Sprintf("%s%d", foundVarName, suffix)) |
| } else { |
| okVarName = fresh(fmt.Sprintf("%s%d", okVarName, suffix)) |
| beforeVarName = fresh(fmt.Sprintf("%s%d", beforeVarName, suffix)) |
| afterVarName = fresh(fmt.Sprintf("%s%d", afterVarName, suffix)) |
| } |
| } |
| |
| // If there will be no uses of ok, before, or after, use the |
| // blank identifier instead. |
| if len(negative) == 0 && len(nonnegative) == 0 { |
| okVarName = "_" |
| } |
| if len(beforeSlice) == 0 { |
| beforeVarName = "_" |
| } |
| if len(afterSlice) == 0 { |
| afterVarName = "_" |
| } |
| |
| var edits []analysis.TextEdit |
| replace := func(exprs []ast.Expr, new string) { |
| for _, expr := range exprs { |
| edits = append(edits, analysis.TextEdit{ |
| Pos: expr.Pos(), |
| End: expr.End(), |
| NewText: []byte(new), |
| }) |
| } |
| } |
| // Get the ident for the call to strings.Index, which could just be |
| // "Index" if the strings package is dot imported. |
| indexCallId := typesinternal.UsedIdent(info, indexCall.Fun) |
| replacedFunc := "Cut" |
| if isContains { |
| replacedFunc = "Contains" |
| replace(negative, "!"+foundVarName) // idx < 0 -> !found |
| replace(nonnegative, foundVarName) // idx > -1 -> found |
| |
| // Replace the assignment with found, and replace the call to |
| // Index or IndexByte with a call to Contains. |
| // i := strings.Index (...) |
| // ----- -------- |
| // found := strings.Contains(...) |
| edits = append(edits, analysis.TextEdit{ |
| Pos: iIdent.Pos(), |
| End: iIdent.End(), |
| NewText: []byte(foundVarName), |
| }, analysis.TextEdit{ |
| Pos: indexCallId.Pos(), |
| End: indexCallId.End(), |
| NewText: []byte("Contains"), |
| }) |
| } else { |
| replace(negative, "!"+okVarName) // idx < 0 -> !ok |
| replace(nonnegative, okVarName) // idx > -1 -> ok |
| replace(beforeSlice, beforeVarName) // s[:idx] -> before |
| replace(afterSlice, afterVarName) // s[idx+k:] -> after |
| |
| // Replace the assignment with before, after, ok, and replace |
| // the call to Index or IndexByte with a call to Cut. |
| // i := strings.Index(...) |
| // ----------------- ----- |
| // before, after, ok := strings.Cut (...) |
| edits = append(edits, analysis.TextEdit{ |
| Pos: iIdent.Pos(), |
| End: iIdent.End(), |
| NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName), |
| }, analysis.TextEdit{ |
| Pos: indexCallId.Pos(), |
| End: indexCallId.End(), |
| NewText: []byte("Cut"), |
| }) |
| } |
| |
| // Calls to IndexByte have a byte as their second arg, which |
| // must be converted to a string or []byte to be a valid arg for Cut/Contains. |
| if obj.Name() == "IndexByte" { |
| switch obj.Pkg().Name() { |
| case "strings": |
| searchByteVal := info.Types[substr].Value |
| if searchByteVal == nil { |
| // substr is a variable, e.g. substr := byte('b') |
| // use string(substr) |
| edits = append(edits, []analysis.TextEdit{ |
| { |
| Pos: substr.Pos(), |
| NewText: []byte("string("), |
| }, |
| { |
| Pos: substr.End(), |
| NewText: []byte(")"), |
| }, |
| }...) |
| } else { |
| // substr is a byte constant |
| val, _ := constant.Int64Val(searchByteVal) // inv: must be a valid byte |
| // strings.Cut/Contains requires a string, so convert byte literal to string literal; e.g. 'a' -> "a", 55 -> "7" |
| edits = append(edits, analysis.TextEdit{ |
| Pos: substr.Pos(), |
| End: substr.End(), |
| NewText: strconv.AppendQuote(nil, string(byte(val))), |
| }) |
| } |
| case "bytes": |
| // bytes.Cut/Contains requires a []byte, so wrap substr in a []byte{} |
| edits = append(edits, []analysis.TextEdit{ |
| { |
| Pos: substr.Pos(), |
| NewText: []byte("[]byte{"), |
| }, |
| { |
| Pos: substr.End(), |
| NewText: []byte("}"), |
| }, |
| }...) |
| } |
| } |
| scopeFixCount[scope]++ |
| pass.Report(analysis.Diagnostic{ |
| Pos: indexCall.Fun.Pos(), |
| End: indexCall.Fun.End(), |
| Message: fmt.Sprintf("%s.%s can be simplified using %s.%s", |
| obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc), |
| Category: "stringscut", |
| SuggestedFixes: []analysis.SuggestedFix{{ |
| Message: fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc), |
| TextEdits: edits, |
| }}, |
| }) |
| } |
| } |
| |
| return nil, nil |
| } |
| |
| // indexArgValid reports whether expr is a valid strings.Index(_, _) arg |
| // for the transformation. An arg is valid iff it is: |
| // - constant; |
| // - a local variable with no modifying uses after the Index() call; or |
| // - []byte(x) where x is also valid by this definition. |
| // All other expressions are assumed not referentially transparent, |
| // so we cannot be sure that all uses are safe to replace. |
| func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool { |
| tv := info.Types[expr] |
| if tv.Value != nil { |
| return true // constant |
| } |
| switch expr := expr.(type) { |
| case *ast.CallExpr: |
| return types.Identical(tv.Type, byteSliceType) && |
| info.Types[expr.Fun].IsType() && // make sure this isn't a function that returns a byte slice |
| indexArgValid(info, index, expr.Args[0], afterPos) // check s in []byte(s) |
| case *ast.Ident: |
| sObj := info.Uses[expr] |
| sUses := index.Uses(sObj) |
| return !hasModifyingUses(info, sUses, afterPos) |
| default: |
| // For now, skip instances where s or substr are not |
| // identifers, basic lits, or call expressions of the form |
| // []byte(s). |
| // TODO(mkalil): Handle s and substr being expressions like ptr.field[i]. |
| // From adonovan: We'd need to 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. |
| return false |
| } |
| } |
| |
| // checkIdxUses inspects the uses of i to make sure they match certain criteria that |
| // allows us to suggest a modernization. If all uses of i, s and substr match |
| // one of the following four valid formats, it returns a list of occurrences for |
| // each format. If any of the uses do not match one of the formats, return nil |
| // for all values, since we should not offer a replacement. |
| // 1. negative - a condition equivalent to i < 0 |
| // 2. nonnegative - a condition equivalent to i >= 0 |
| // 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i] |
| // 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr)) |
| // |
| // Additionally, all beforeSlice and afterSlice uses must be dominated by a |
| // nonnegative guard on i (i.e., inside the body of an if whose condition |
| // checks i >= 0, or in the else of a negative check, or after an |
| // early-return negative check). This ensures that the rewrite from |
| // s[i+len(sep):] to "after" preserves semantics, since when i == -1, |
| // s[i+len(sep):] may yield a valid substring (e.g. s[0:] for single-byte |
| // separators), but "after" would be "". |
| // |
| // When len(substr)==1, it's safe to use s[i+1:] even when i < 0. |
| // Otherwise, each replacement of s[i+1:] must be guarded by a check |
| // that i is nonnegative. |
| func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr, iObj types.Object) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) { |
| requireGuard := true |
| if l := constSubstrLen(info, substr); l != -1 && l != 1 { |
| requireGuard = false |
| } |
| |
| use := func(cur inspector.Cursor) bool { |
| ek := cur.ParentEdgeKind() |
| n := cur.Parent().Node() |
| switch ek { |
| case edge.BinaryExpr_X, edge.BinaryExpr_Y: |
| check := n.(*ast.BinaryExpr) |
| switch checkIdxComparison(info, check, iObj) { |
| case -1: |
| negative = append(negative, check) |
| return true |
| case 1: |
| nonnegative = append(nonnegative, check) |
| return true |
| } |
| // Check is not equivalent to that i < 0 or i >= 0. |
| // Might be part of an outer slice expression like s[i + k] |
| // which requires a different check. |
| // Check that the thing being sliced is s and that the slice |
| // doesn't have a max index. |
| if slice, ok := cur.Parent().Parent().Node().(*ast.SliceExpr); ok && |
| sameObject(info, s, slice.X) && |
| slice.Max == nil { |
| if isBeforeSlice(info, ek, slice) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) { |
| beforeSlice = append(beforeSlice, slice) |
| return true |
| } else if isAfterSlice(info, ek, slice, substr) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) { |
| afterSlice = append(afterSlice, slice) |
| return true |
| } |
| } |
| case edge.SliceExpr_Low, edge.SliceExpr_High: |
| slice := n.(*ast.SliceExpr) |
| // Check that the thing being sliced is s and that the slice doesn't |
| // have a max index. |
| if sameObject(info, s, slice.X) && slice.Max == nil { |
| if isBeforeSlice(info, ek, slice) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) { |
| beforeSlice = append(beforeSlice, slice) |
| return true |
| } else if isAfterSlice(info, ek, slice, substr) && (!requireGuard || isSliceIndexGuarded(info, cur, iObj)) { |
| afterSlice = append(afterSlice, slice) |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| for curIdent := range uses { |
| if !use(curIdent) { |
| return nil, nil, nil, nil |
| } |
| } |
| return negative, nonnegative, beforeSlice, afterSlice |
| } |
| |
| // hasModifyingUses reports whether any of the uses involve potential |
| // modifications. Uses involving assignments before the "afterPos" won't be |
| // considered. |
| func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool { |
| for curUse := range uses { |
| ek := curUse.ParentEdgeKind() |
| if ek == edge.AssignStmt_Lhs { |
| if curUse.Node().Pos() <= afterPos { |
| continue |
| } |
| assign := curUse.Parent().Node().(*ast.AssignStmt) |
| if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) { |
| // Modifying use because we are reassigning the value of the object. |
| return true |
| } |
| } else if ek == edge.UnaryExpr_X && |
| curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND { |
| // Modifying use because we might be passing the object by reference (an explicit &). |
| // We can ignore the case where we have a method call on the expression (which |
| // has an implicit &) because we know the type of s and substr are strings |
| // which cannot have methods on them. |
| return true |
| } |
| } |
| return false |
| } |
| |
| // checkIdxComparison reports whether the check is equivalent to i < 0 or its negation, or neither. |
| // For equivalent to i >= 0, we only accept this exact BinaryExpr since |
| // expressions like i > 0 or i >= 1 make a stronger statement about the value of i. |
| // We avoid suggesting a fix in this case since it may result in an invalid |
| // transformation (See golang/go#76687). |
| // Since strings.Index returns exactly -1 if the substring is not found, we |
| // don't need to handle expressions like i <= -3. |
| // We return 0 if the expression does not match any of these options. |
| func checkIdxComparison(info *types.Info, check *ast.BinaryExpr, iObj types.Object) int { |
| isI := func(e ast.Expr) bool { |
| id, ok := e.(*ast.Ident) |
| return ok && info.Uses[id] == iObj |
| } |
| if !isI(check.X) && !isI(check.Y) { |
| return 0 |
| } |
| |
| // Ensure that the constant (if any) is on the right. |
| x, op, y := check.X, check.Op, check.Y |
| if info.Types[x].Value != nil { |
| x, op, y = y, flip(op), x |
| } |
| |
| yIsInt := func(k int64) bool { |
| return isIntLiteral(info, y, k) |
| } |
| |
| if op == token.LSS && yIsInt(0) || // i < 0 |
| op == token.EQL && yIsInt(-1) || // i == -1 |
| op == token.LEQ && yIsInt(-1) { // i <= -1 |
| return -1 // check <=> i is negative |
| } |
| |
| if op == token.GEQ && yIsInt(0) || // i >= 0 |
| op == token.NEQ && yIsInt(-1) || // i != -1 |
| op == token.GTR && yIsInt(-1) { // i > -1 |
| return +1 // check <=> i is non-negative |
| } |
| |
| return 0 // unknown |
| } |
| |
| // flip changes the comparison token as if the operands were flipped. |
| // It is defined only for == and the four inequalities. |
| func flip(op token.Token) token.Token { |
| switch op { |
| case token.EQL: |
| return token.EQL // (same) |
| case token.GEQ: |
| return token.LEQ |
| case token.GTR: |
| return token.LSS |
| case token.LEQ: |
| return token.GEQ |
| case token.LSS: |
| return token.GTR |
| } |
| return op |
| } |
| |
| // isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i]. |
| func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool { |
| return ek == edge.SliceExpr_High && (slice.Low == nil || isZeroIntConst(info, slice.Low)) |
| } |
| |
| // constSubstrLen returns the constant length of substr, or -1 if unknown. |
| func constSubstrLen(info *types.Info, substr ast.Expr) int { |
| // Handle len([]byte(substr)) |
| if call, ok := substr.(*ast.CallExpr); ok { |
| tv := info.Types[call.Fun] |
| if tv.IsType() && types.Identical(tv.Type, byteSliceType) { |
| // Only one arg in []byte conversion. |
| substr = call.Args[0] |
| } |
| } |
| substrVal := info.Types[substr].Value |
| if substrVal != nil { |
| switch substrVal.Kind() { |
| case constant.String: |
| return len(constant.StringVal(substrVal)) |
| case constant.Int: |
| // constant.Value is a byte literal, e.g. bytes.IndexByte(_, 'a') |
| // or a numeric byte literal, e.g. bytes.IndexByte(_, 65) |
| // ([]byte(rune) is not legal.) |
| return 1 |
| } |
| } |
| return -1 |
| } |
| |
| // isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):], |
| // or s[i + k:] where k is a const is equal to len(substr). |
| func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr ast.Expr) bool { |
| lowExpr, ok := slice.Low.(*ast.BinaryExpr) |
| if !ok || slice.High != nil { |
| return false |
| } |
| // Returns true if the expression is a call to len(substr). |
| isLenCall := func(expr ast.Expr) bool { |
| call, ok := expr.(*ast.CallExpr) |
| if !ok || len(call.Args) != 1 { |
| return false |
| } |
| return sameObject(info, substr, call.Args[0]) && typeutil.Callee(info, call) == builtinLen |
| } |
| |
| substrLen := constSubstrLen(info, substr) |
| |
| switch ek { |
| case edge.BinaryExpr_X: |
| kVal := info.Types[lowExpr.Y].Value |
| if kVal == nil { |
| // i + len(substr) |
| return lowExpr.Op == token.ADD && isLenCall(lowExpr.Y) |
| } else { |
| // i + k |
| kInt, ok := constant.Int64Val(kVal) |
| return ok && substrLen == int(kInt) |
| } |
| case edge.BinaryExpr_Y: |
| kVal := info.Types[lowExpr.X].Value |
| if kVal == nil { |
| // len(substr) + i |
| return lowExpr.Op == token.ADD && isLenCall(lowExpr.X) |
| } else { |
| // k + i |
| kInt, ok := constant.Int64Val(kVal) |
| return ok && substrLen == int(kInt) |
| } |
| } |
| return false |
| } |
| |
| // isSliceIndexGuarded reports whether a use of the index variable i (at the given cursor) |
| // inside a slice expression is dominated by a nonnegative guard. |
| // A use is considered guarded if any of the following are true: |
| // - It is inside the Body of an IfStmt whose condition is a nonnegative check on i. |
| // - It is inside the Else of an IfStmt whose condition is a negative check on i. |
| // - It is preceded (in the same block) by an IfStmt whose condition is a |
| // negative check on i with a terminating body (e.g., early return). |
| // |
| // Conversely, a use is immediately rejected if: |
| // - It is inside the Body of an IfStmt whose condition is a negative check on i. |
| // - It is inside the Else of an IfStmt whose condition is a nonnegative check on i. |
| // |
| // We have already checked (see [hasModifyingUses]) that there are no |
| // intervening uses (incl. via aliases) of i that might alter its value. |
| func isSliceIndexGuarded(info *types.Info, cur inspector.Cursor, iObj types.Object) bool { |
| for anc := range cur.Enclosing() { |
| switch anc.ParentEdgeKind() { |
| case edge.IfStmt_Body, edge.IfStmt_Else: |
| ifStmt := anc.Parent().Node().(*ast.IfStmt) |
| check := condChecksIdx(info, ifStmt.Cond, iObj) |
| if anc.ParentEdgeKind() == edge.IfStmt_Else { |
| check = -check |
| } |
| if check > 0 { |
| return true // inside nonnegative-guarded block (i >= 0 here) |
| } |
| if check < 0 { |
| return false // inside negative-guarded block (i < 0 here) |
| } |
| case edge.BlockStmt_List: |
| // Check preceding siblings for early-return negative checks. |
| for sib, ok := anc.PrevSibling(); ok; sib, ok = sib.PrevSibling() { |
| ifStmt, ok := sib.Node().(*ast.IfStmt) |
| if ok && condChecksIdx(info, ifStmt.Cond, iObj) < 0 && bodyTerminates(ifStmt.Body) { |
| return true // preceded by early-return negative check |
| } |
| } |
| case edge.FuncDecl_Body, edge.FuncLit_Body: |
| return false // stop at function boundary |
| } |
| } |
| return false |
| } |
| |
| // condChecksIdx reports whether cond is a BinaryExpr that checks |
| // the index variable iObj for negativity or non-negativity. |
| // Returns -1 for negative (e.g. i < 0), +1 for nonnegative (e.g. i >= 0), 0 otherwise. |
| func condChecksIdx(info *types.Info, cond ast.Expr, iObj types.Object) int { |
| binExpr, ok := cond.(*ast.BinaryExpr) |
| if !ok { |
| return 0 |
| } |
| return checkIdxComparison(info, binExpr, iObj) |
| } |
| |
| // bodyTerminates reports whether the given block statement unconditionally |
| // terminates execution (via return, break, continue, or goto). |
| func bodyTerminates(block *ast.BlockStmt) bool { |
| if len(block.List) == 0 { |
| return false |
| } |
| last := block.List[len(block.List)-1] |
| switch last.(type) { |
| case *ast.ReturnStmt, *ast.BranchStmt: |
| return true // return, break, continue, goto |
| } |
| return false |
| } |
| |
| // sameObject reports whether we know that the expressions resolve to the same object. |
| func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool { |
| if ident1, ok := expr1.(*ast.Ident); ok { |
| if ident2, ok := expr2.(*ast.Ident); ok { |
| uses1, ok1 := info.Uses[ident1] |
| uses2, ok2 := info.Uses[ident2] |
| return ok1 && ok2 && uses1 == uses2 |
| } |
| } |
| return false |
| } |