1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/token"
11 "go/types"
12 "strings"
13
14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/ast/edge"
17 "golang.org/x/tools/go/types/typeutil"
18 "golang.org/x/tools/internal/analysis/analyzerutil"
19 typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
20 "golang.org/x/tools/internal/astutil"
21 "golang.org/x/tools/internal/refactor"
22 "golang.org/x/tools/internal/typesinternal"
23 "golang.org/x/tools/internal/typesinternal/typeindex"
24 "golang.org/x/tools/internal/versions"
25 )
26
27
28 var slicesBackwardAnalyzer = &analysis.Analyzer{
29 Name: "slicesbackward",
30 Doc: analyzerutil.MustExtractDoc(doc, "slicesbackward"),
31 Requires: []*analysis.Analyzer{
32 inspect.Analyzer,
33 typeindexanalyzer.Analyzer,
34 },
35 Run: slicesbackward,
36 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesbackward",
37 }
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 func slicesbackward(pass *analysis.Pass) (any, error) {
56
57
58 if within(pass, "slices") {
59 return nil, nil
60 }
61
62 var (
63 info = pass.TypesInfo
64 index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
65 )
66
67 for curFile := range filesUsingGoVersion(pass, versions.Go1_23) {
68 file := curFile.Node().(*ast.File)
69
70 nextLoop:
71 for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
72 loop := curLoop.Node().(*ast.ForStmt)
73
74
75 init, ok := loop.Init.(*ast.AssignStmt)
76 if !ok || !isSimpleAssign(init) {
77 continue
78 }
79 indexIdent, ok := init.Lhs[0].(*ast.Ident)
80 if !ok {
81 continue
82 }
83 indexObj := info.ObjectOf(indexIdent).(*types.Var)
84
85
86 binRhs, ok := init.Rhs[0].(*ast.BinaryExpr)
87 if !ok || binRhs.Op != token.SUB {
88 continue
89 }
90 if !isIntLiteral(info, binRhs.Y, 1) {
91 continue
92 }
93 lenCall, ok := binRhs.X.(*ast.CallExpr)
94 if !ok || typeutil.Callee(info, lenCall) != builtinLen {
95 continue
96 }
97 if len(lenCall.Args) != 1 {
98 continue
99 }
100 sliceExpr := lenCall.Args[0]
101 if _, ok := info.TypeOf(sliceExpr).Underlying().(*types.Slice); !ok {
102 continue
103 }
104
105
106 cond, ok := loop.Cond.(*ast.BinaryExpr)
107 if !ok || cond.Op != token.GEQ {
108 continue
109 }
110 if !astutil.EqualSyntax(cond.X, indexIdent) {
111 continue
112 }
113 if !isZeroIntConst(info, cond.Y) {
114 continue
115 }
116
117
118 dec, ok := loop.Post.(*ast.IncDecStmt)
119 if !ok || dec.Tok != token.DEC {
120 continue
121 }
122 if !astutil.EqualSyntax(dec.X, indexIdent) {
123 continue
124 }
125
126
127
128
129
130 bodyCur := curLoop.Child(loop.Body)
131 for curUse := range index.Uses(indexObj) {
132 if !typesinternal.IsAssignedOrAddressTaken(info, curUse) {
133 continue
134 }
135 if bodyCur.Contains(curUse) {
136 continue nextLoop
137 }
138 if init.Tok == token.ASSIGN && !curLoop.Contains(curUse) {
139 continue nextLoop
140 }
141 }
142
143
144
145
146 var (
147
148 firstSliceIdxAssign *ast.AssignStmt
149
150 sliceIdxsReplace []*ast.IndexExpr
151
152 sliceIdxs int
153
154 otherUses int
155 )
156 for curUse := range index.Uses(indexObj) {
157 if !bodyCur.Contains(curUse) {
158 continue
159 }
160
161
162
163
164
165
166
167
168
169 if curUse.ParentEdgeKind() == edge.IndexExpr_Index {
170 curIdx := curUse.Parent()
171 if typesinternal.IsAssignedOrAddressTaken(info, curIdx) {
172 continue nextLoop
173 }
174 idxExpr := curIdx.Node().(*ast.IndexExpr)
175 if astutil.EqualSyntax(idxExpr.X, sliceExpr) {
176 sliceIdxs++
177
178
179
180
181 if firstSliceIdxAssign == nil && curIdx.ParentEdgeKind() == edge.AssignStmt_Rhs {
182 assignStmt := curIdx.Parent().Node().(*ast.AssignStmt)
183 if len(assignStmt.Lhs) == 1 && assignStmt.Tok == token.DEFINE {
184
185
186 firstSliceIdxAssign = assignStmt
187
188
189 continue
190 }
191 }
192 sliceIdxsReplace = append(sliceIdxsReplace, idxExpr)
193 continue
194 }
195 }
196 otherUses++
197 }
198
199
200
201
202
203
204 sliceStr := astutil.Format(pass.Fset, sliceExpr)
205 prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Backward", loop.Pos())
206 elemName := chooseValueName(firstSliceIdxAssign, sliceStr)
207 elemName = freshName(info, index, info.Scopes[loop], loop.Pos(), bodyCur, bodyCur, token.NoPos, elemName)
208
209
210
211
212 for _, sx := range sliceIdxsReplace {
213 edits = append(edits, analysis.TextEdit{
214 Pos: sx.Pos(),
215 End: sx.End(),
216 NewText: []byte(elemName),
217 })
218 }
219
220 if firstSliceIdxAssign != nil {
221 edits = append(edits, analysis.TextEdit{
222 Pos: firstSliceIdxAssign.Pos(),
223 End: firstSliceIdxAssign.End(),
224 })
225 }
226
227
228
229
230 var vars string
231 if otherUses == 0 {
232
233 vars = fmt.Sprintf("_, %s", elemName)
234 } else if sliceIdxs == 0 {
235
236 vars = indexIdent.Name
237 } else {
238 vars = fmt.Sprintf("%s, %s", indexIdent.Name, elemName)
239 }
240 header := fmt.Sprintf("%s := range %sBackward(%s)", vars, prefix, sliceStr)
241 edits = append(edits, analysis.TextEdit{
242 Pos: loop.Init.Pos(),
243 End: loop.Post.End(),
244 NewText: []byte(header),
245 })
246
247 pass.Report(analysis.Diagnostic{
248 Pos: loop.Init.Pos(),
249 End: loop.Post.End(),
250 Message: "backward loop over slice can be modernized using slices.Backward",
251 SuggestedFixes: []analysis.SuggestedFix{{
252 Message: fmt.Sprintf("Replace with range slices.Backward(%s)", sliceStr),
253 TextEdits: edits,
254 }},
255 })
256 }
257 }
258 return nil, nil
259 }
260
261
262
263 func chooseValueName(assign *ast.AssignStmt, sliceStr string) string {
264 if assign != nil {
265 return assign.Lhs[0].(*ast.Ident).Name
266 }
267
268
269 if token.IsIdentifier(sliceStr) && len(sliceStr) > 1 {
270 if single, ok := strings.CutSuffix(sliceStr, "s"); ok {
271 return single
272 }
273 return sliceStr[:1]
274 }
275 return "v"
276 }
277
View as plain text