blob: 53a4363faa21459055dbde61dbaa2c7fd33bdfb6 [file]
// 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/token"
"go/types"
"strings"
"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/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/refactor"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var slicesBackwardAnalyzer = &analysis.Analyzer{
Name: "slicesbackward",
Doc: analyzerutil.MustExtractDoc(doc, "slicesbackward"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: slicesbackward,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.SlicesBackwardModernizer = slicesBackwardAnalyzer
}
// slicesbackward offers a fix to replace a manually-written backward loop:
//
// for i := len(s) - 1; i >= 0; i-- {
// use(s[i])
// }
//
// with a range loop using slices.Backward (added in Go 1.23):
//
// for _, v := range slices.Backward(s) {
// use(v)
// }
//
// If the loop index is needed beyond just indexing into the slice, both
// the index and value variables are kept:
//
// for i, v := range slices.Backward(s) { ... }
func slicesbackward(pass *analysis.Pass) (any, error) {
// Skip packages that are in the slices stdlib dependency tree to
// avoid import cycles.
if within(pass, "slices") {
return nil, nil
}
var (
info = pass.TypesInfo
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
)
for curFile := range filesUsingGoVersion(pass, versions.Go1_23) {
file := curFile.Node().(*ast.File)
nextLoop:
for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
loop := curLoop.Node().(*ast.ForStmt)
// Match init: i := len(s) - 1 or i = len(s) - 1
init, ok := loop.Init.(*ast.AssignStmt)
if !ok || !isSimpleAssign(init) {
continue
}
indexIdent, ok := init.Lhs[0].(*ast.Ident)
if !ok {
continue
}
indexObj := info.ObjectOf(indexIdent).(*types.Var)
// RHS must be len(s) - 1.
binRhs, ok := init.Rhs[0].(*ast.BinaryExpr)
if !ok || binRhs.Op != token.SUB {
continue
}
if !isIntLiteral(info, binRhs.Y, 1) {
continue
}
lenCall, ok := binRhs.X.(*ast.CallExpr)
if !ok || typeutil.Callee(info, lenCall) != builtinLen {
continue
}
if len(lenCall.Args) != 1 {
continue
}
sliceExpr := lenCall.Args[0]
if _, ok := info.TypeOf(sliceExpr).Underlying().(*types.Slice); !ok {
continue
}
// Match cond: i >= 0
cond, ok := loop.Cond.(*ast.BinaryExpr)
if !ok || cond.Op != token.GEQ {
continue
}
if !astutil.EqualSyntax(cond.X, indexIdent) {
continue
}
if !isZeroIntConst(info, cond.Y) {
continue
}
// Match post: i--
dec, ok := loop.Post.(*ast.IncDecStmt)
if !ok || dec.Tok != token.DEC {
continue
}
if !astutil.EqualSyntax(dec.X, indexIdent) {
continue
}
// Check that i is not used as an lvalue in the loop body.
// If init is = (not :=), i is a pre-existing variable; also
// check that it is not used as an lvalue outside the loop
// (e.g. &i before the loop).
bodyCur := curLoop.Child(loop.Body)
for curUse := range index.Uses(indexObj) {
if !isScalarLvalue(info, curUse) {
continue
}
if bodyCur.Contains(curUse) {
continue nextLoop // i is mutated in loop body
}
if init.Tok == token.ASSIGN && !curLoop.Contains(curUse) {
continue nextLoop // pre-existing i is an lvalue outside the loop
}
}
// Find all uses of i in the loop body. Classify as:
// s[i] — pure element accesses that can be replaced by the value var
// other — index used for non-indexing purposes
var (
sliceIndexes []*ast.IndexExpr
otherUses int
)
for curUse := range index.Uses(indexObj) {
if !bodyCur.Contains(curUse) {
continue
}
// Is i in the Index position of an s[i] expression?
// If so, we also need to check whether s[i] is an lvalue. If we're
// mutating the slice or taking an element's address, a fix will not
// be offered.
if curUse.ParentEdgeKind() == edge.IndexExpr_Index {
if isScalarLvalue(pass.TypesInfo, curUse.Parent()) {
continue nextLoop
}
idxExpr := curUse.Parent().Node().(*ast.IndexExpr)
if astutil.EqualSyntax(idxExpr.X, sliceExpr) {
sliceIndexes = append(sliceIndexes, idxExpr)
continue
}
}
otherUses++
}
// Build the suggested fix.
//
// for i := len(s) - 1; i >= 0; i-- { ... s[i] ... }
// ---------------------------- ----
// _, v := range slices.Backward(s) v
sliceStr := astutil.Format(pass.Fset, sliceExpr)
prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Backward", loop.Pos())
// A simple heuristic for choosing the name of the value variable: attempt
// to take the singular form of the of the variable used in the call to
// slices.Backward by trimming any suffix "s". If that fails, use the
// first character of that variable. If that fails, fallback to "v".
s := strings.TrimSuffix(sliceStr, "s")
if s == sliceStr {
s = sliceStr[0:1]
}
if s == "" || s == sliceStr {
s = "v"
}
elemName := freshName(info, index, info.Scopes[loop], loop.Pos(), bodyCur, bodyCur, token.NoPos, s)
// Replace each s[i] with elemName.
for _, sx := range sliceIndexes {
edits = append(edits, analysis.TextEdit{
Pos: sx.Pos(),
End: sx.End(),
NewText: []byte(elemName),
})
}
// Replace the loop header with a range over slices.Backward. In
// well-typed code, at least one of the index or value variables must be
// referenced inside the loop body (otherUses + sliceIndexes > 0).
var vars string
if otherUses == 0 { // sliceIndexes > 0
// All uses of i are s[i]; drop the index variable.
vars = fmt.Sprintf("_, %s", elemName)
} else if len(sliceIndexes) == 0 { // otherUses > 0
// Index i is not used in any s[i] expressions; drop the value variable.
vars = indexIdent.Name
} else { // otherUses > 0 && sliceIndexes > 0, keep both variables.
vars = fmt.Sprintf("%s, %s", indexIdent.Name, elemName)
}
header := fmt.Sprintf("%s := range %sBackward(%s)", vars, prefix, sliceStr)
edits = append(edits, analysis.TextEdit{
Pos: loop.Init.Pos(),
End: loop.Post.End(),
NewText: []byte(header),
})
pass.Report(analysis.Diagnostic{
Pos: loop.Init.Pos(),
End: loop.Post.End(),
Message: "backward loop over slice can be modernized using slices.Backward",
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace with range slices.Backward(%s)", sliceStr),
TextEdits: edits,
}},
})
}
}
return nil, nil
}