1
2
3
4
5 package modernize
6
7 import (
8 "fmt"
9 "go/ast"
10 "go/token"
11 "go/types"
12
13 "golang.org/x/tools/go/analysis"
14 "golang.org/x/tools/go/analysis/passes/inspect"
15 "golang.org/x/tools/go/ast/edge"
16 "golang.org/x/tools/go/ast/inspector"
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/moreiters"
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 var RangeIntAnalyzer = &analysis.Analyzer{
28 Name: "rangeint",
29 Doc: analyzerutil.MustExtractDoc(doc, "rangeint"),
30 Requires: []*analysis.Analyzer{
31 inspect.Analyzer,
32 typeindexanalyzer.Analyzer,
33 },
34 Run: rangeint,
35 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint",
36 }
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 func rangeint(pass *analysis.Pass) (any, error) {
69 var (
70 info = pass.TypesInfo
71 typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
72 )
73
74 for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
75 nextLoop:
76 for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
77 loop := curLoop.Node().(*ast.ForStmt)
78 if init, ok := loop.Init.(*ast.AssignStmt); ok &&
79 isSimpleAssign(init) &&
80 is[*ast.Ident](init.Lhs[0]) &&
81 isZeroIntConst(info, init.Rhs[0]) {
82
83 index := init.Lhs[0].(*ast.Ident)
84
85 if compare, ok := loop.Cond.(*ast.BinaryExpr); ok &&
86 compare.Op == token.LSS &&
87 astutil.EqualSyntax(compare.X, init.Lhs[0]) {
88
89
90 limit := compare.Y
91
92
93
94
95
96
97 if call, ok := limit.(*ast.CallExpr); ok &&
98 typeutil.Callee(info, call) == builtinLen &&
99 is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
100 limit = call.Args[0]
101 }
102
103
104
105 limitOK := false
106 if info.Types[limit].Value != nil {
107 limitOK = true
108 } else if id, ok := limit.(*ast.Ident); ok {
109 if v, ok := info.Uses[id].(*types.Var); ok &&
110 !(v.Exported() && typesinternal.IsPackageLevel(v)) {
111
112
113 for cur := range typeindex.Uses(v) {
114 if isScalarLvalue(info, cur) {
115
116 continue nextLoop
117 }
118 }
119 limitOK = true
120 }
121 }
122 if !limitOK {
123 continue nextLoop
124 }
125
126 validIncrement := false
127 if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
128 inc.Tok == token.INC &&
129 astutil.EqualSyntax(compare.X, inc.X) {
130
131 validIncrement = true
132 } else if assign, ok := loop.Post.(*ast.AssignStmt); ok &&
133 assign.Tok == token.ADD_ASSIGN &&
134 len(assign.Rhs) == 1 && isIntLiteral(info, assign.Rhs[0], 1) &&
135 len(assign.Lhs) == 1 && astutil.EqualSyntax(compare.X, assign.Lhs[0]) {
136
137 validIncrement = true
138 }
139
140 if validIncrement {
141
142
143
144 v := info.ObjectOf(index).(*types.Var)
145
146 if typesinternal.IsPackageLevel(v) {
147 continue nextLoop
148 }
149
150
151
152
153 if moreiters.Contains(enclosingSignature(curLoop, info).Results().Variables(), v) {
154 continue nextLoop
155 }
156
157 used := false
158 for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
159 id := curId.Node().(*ast.Ident)
160 if info.Uses[id] == v {
161 used = true
162
163
164
165
166 if isScalarLvalue(info, curId) {
167 continue nextLoop
168 }
169 }
170 }
171
172
173 var edits []analysis.TextEdit
174 if !used && init.Tok == token.DEFINE {
175 edits = append(edits, analysis.TextEdit{
176 Pos: index.Pos(),
177 End: init.Rhs[0].Pos(),
178 })
179 }
180
181
182
183
184 if init.Tok == token.ASSIGN {
185
186
187
188
189
190
191
192
193
194
195
196 ancestor := curLoop.Parent()
197 for is[*ast.LabeledStmt](ancestor.Node()) {
198 ancestor = ancestor.Parent()
199 }
200 for curId := range ancestor.Preorder((*ast.Ident)(nil)) {
201 id := curId.Node().(*ast.Ident)
202 if info.Uses[id] == v {
203
204 if id.Pos() > loop.End() {
205 continue nextLoop
206 }
207
208
209
210
211
212 for curDefer := range curId.Enclosing((*ast.DeferStmt)(nil)) {
213 if curDefer.Node().Pos() > v.Pos() {
214 continue nextLoop
215 }
216 }
217 }
218 }
219 }
220
221
222
223 if call, ok := limit.(*ast.CallExpr); ok &&
224 typeutil.Callee(info, call) == builtinLen &&
225 is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
226 limit = call.Args[0]
227 }
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242 var beforeLimit, afterLimit string
243 if v := info.Types[limit].Value; v != nil {
244 tVar := info.TypeOf(init.Rhs[0])
245 file := curFile.Node().(*ast.File)
246
247
248 qual := typesinternal.FileQualifier(file, pass.Pkg)
249 beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")"
250 info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
251 if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
252 tLimit := types.Default(info2.TypeOf(limit))
253 if types.AssignableTo(tLimit, tVar) {
254 beforeLimit, afterLimit = "", ""
255 }
256 }
257 }
258
259 pass.Report(analysis.Diagnostic{
260 Pos: init.Pos(),
261 End: loop.Post.End(),
262 Message: "for loop can be modernized using range over int",
263 SuggestedFixes: []analysis.SuggestedFix{{
264 Message: fmt.Sprintf("Replace for loop with range %s",
265 astutil.Format(pass.Fset, limit)),
266 TextEdits: append(edits, []analysis.TextEdit{
267
268
269
270
271
272
273 {
274 Pos: init.Rhs[0].Pos(),
275 End: limit.Pos(),
276 NewText: []byte("range "),
277 },
278
279 {
280 Pos: limit.Pos(),
281 End: limit.Pos(),
282 NewText: []byte(beforeLimit),
283 },
284
285 {
286 Pos: limit.End(),
287 End: loop.Post.End(),
288 },
289
290 {
291 Pos: limit.End(),
292 End: limit.End(),
293 NewText: []byte(afterLimit),
294 },
295 }...),
296 }},
297 })
298 }
299 }
300 }
301 }
302 }
303 return nil, nil
304 }
305
306
307
308
309
310
311 func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool {
312
313
314
315
316 cur := curId
317
318
319 ek, _ := cur.ParentEdge()
320 for ek == edge.ParenExpr_X {
321 cur = cur.Parent()
322 ek, _ = cur.ParentEdge()
323 }
324
325 switch ek {
326 case edge.AssignStmt_Lhs:
327 assign := cur.Parent().Node().(*ast.AssignStmt)
328 if assign.Tok != token.DEFINE {
329 return true
330 }
331 id := curId.Node().(*ast.Ident)
332 if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() {
333 return true
334 }
335 case edge.RangeStmt_Key:
336 rng := cur.Parent().Node().(*ast.RangeStmt)
337 if rng.Tok == token.ASSIGN {
338 return true
339 }
340 case edge.IncDecStmt_X:
341 return true
342 case edge.UnaryExpr_X:
343 if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
344 return true
345 }
346 }
347 return false
348 }
349
350
351
352
353
354
355 func enclosingSignature(cur inspector.Cursor, info *types.Info) *types.Signature {
356 if c, ok := enclosingFunc(cur); ok {
357 switch n := c.Node().(type) {
358 case *ast.FuncDecl:
359 if f, ok := info.Defs[n.Name]; ok {
360 return f.Type().(*types.Signature)
361 }
362 case *ast.FuncLit:
363 if f, ok := info.Types[n]; ok {
364 return f.Type.(*types.Signature)
365 }
366 }
367 }
368 return nil
369 }
370
View as plain text