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