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