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/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 isScalarLvalue(info, cur) {
114
115 continue nextLoop
116 }
117 }
118 limitOK = true
119 }
120 }
121 if !limitOK {
122 continue nextLoop
123 }
124
125 if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
126 inc.Tok == token.INC &&
127 astutil.EqualSyntax(compare.X, inc.X) {
128
129
130
131 v := info.ObjectOf(index).(*types.Var)
132
133 if typesinternal.IsPackageLevel(v) {
134 continue nextLoop
135 }
136 used := false
137 for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
138 id := curId.Node().(*ast.Ident)
139 if info.Uses[id] == v {
140 used = true
141
142
143
144
145 if isScalarLvalue(info, curId) {
146 continue nextLoop
147 }
148 }
149 }
150
151
152 var edits []analysis.TextEdit
153 if !used && init.Tok == token.DEFINE {
154 edits = append(edits, analysis.TextEdit{
155 Pos: index.Pos(),
156 End: init.Rhs[0].Pos(),
157 })
158 }
159
160
161
162
163 if init.Tok == token.ASSIGN {
164
165
166
167
168
169
170
171
172
173
174
175 ancestor := curLoop.Parent()
176 for is[*ast.LabeledStmt](ancestor.Node()) {
177 ancestor = ancestor.Parent()
178 }
179 for curId := range ancestor.Preorder((*ast.Ident)(nil)) {
180 id := curId.Node().(*ast.Ident)
181 if info.Uses[id] == v {
182
183 if id.Pos() > loop.End() {
184 continue nextLoop
185 }
186
187
188
189
190
191 for curDefer := range curId.Enclosing((*ast.DeferStmt)(nil)) {
192 if curDefer.Node().Pos() > v.Pos() {
193 continue nextLoop
194 }
195 }
196 }
197 }
198 }
199
200
201
202 if call, ok := limit.(*ast.CallExpr); ok &&
203 typeutil.Callee(info, call) == builtinLen &&
204 is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
205 limit = call.Args[0]
206 }
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221 var beforeLimit, afterLimit string
222 if v := info.Types[limit].Value; v != nil {
223 tVar := info.TypeOf(init.Rhs[0])
224 file := curFile.Node().(*ast.File)
225
226
227 qual := typesinternal.FileQualifier(file, pass.Pkg)
228 beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")"
229 info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
230 if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
231 tLimit := info2.TypeOf(limit)
232
233
234
235
236
237
238
239
240 if isInteger(tLimit) {
241
242
243 if init.Tok != token.ASSIGN {
244 tLimit = types.Default(tLimit)
245 }
246 if types.AssignableTo(tLimit, tVar) {
247 beforeLimit, afterLimit = "", ""
248 }
249 }
250 }
251 }
252
253 pass.Report(analysis.Diagnostic{
254 Pos: init.Pos(),
255 End: inc.End(),
256 Message: "for loop can be modernized using range over int",
257 SuggestedFixes: []analysis.SuggestedFix{{
258 Message: fmt.Sprintf("Replace for loop with range %s",
259 astutil.Format(pass.Fset, limit)),
260 TextEdits: append(edits, []analysis.TextEdit{
261
262
263
264
265
266
267 {
268 Pos: init.Rhs[0].Pos(),
269 End: limit.Pos(),
270 NewText: []byte("range "),
271 },
272
273 {
274 Pos: limit.Pos(),
275 End: limit.Pos(),
276 NewText: []byte(beforeLimit),
277 },
278
279 {
280 Pos: limit.End(),
281 End: inc.End(),
282 },
283
284 {
285 Pos: limit.End(),
286 End: limit.End(),
287 NewText: []byte(afterLimit),
288 },
289 }...),
290 }},
291 })
292 }
293 }
294 }
295 }
296 }
297 return nil, nil
298 }
299
300
301
302
303
304
305 func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool {
306
307
308
309
310 cur := curId
311
312
313 ek, _ := cur.ParentEdge()
314 for ek == edge.ParenExpr_X {
315 cur = cur.Parent()
316 ek, _ = cur.ParentEdge()
317 }
318
319 switch ek {
320 case edge.AssignStmt_Lhs:
321 assign := cur.Parent().Node().(*ast.AssignStmt)
322 if assign.Tok != token.DEFINE {
323 return true
324 }
325 id := curId.Node().(*ast.Ident)
326 if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() {
327 return true
328 }
329 case edge.RangeStmt_Key:
330 rng := cur.Parent().Node().(*ast.RangeStmt)
331 if rng.Tok == token.ASSIGN {
332 return true
333 }
334 case edge.IncDecStmt_X:
335 return true
336 case edge.UnaryExpr_X:
337 if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
338 return true
339 }
340 }
341 return false
342 }
343
344 func isInteger(t types.Type) bool {
345 basic, ok := t.Underlying().(*types.Basic)
346 return ok && basic.Info()&types.IsInteger != 0
347 }
348
View as plain text