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 := types.Default(info2.TypeOf(limit))
232 if types.AssignableTo(tLimit, tVar) {
233 beforeLimit, afterLimit = "", ""
234 }
235 }
236 }
237
238 pass.Report(analysis.Diagnostic{
239 Pos: init.Pos(),
240 End: inc.End(),
241 Message: "for loop can be modernized using range over int",
242 SuggestedFixes: []analysis.SuggestedFix{{
243 Message: fmt.Sprintf("Replace for loop with range %s",
244 astutil.Format(pass.Fset, limit)),
245 TextEdits: append(edits, []analysis.TextEdit{
246
247
248
249
250
251
252 {
253 Pos: init.Rhs[0].Pos(),
254 End: limit.Pos(),
255 NewText: []byte("range "),
256 },
257
258 {
259 Pos: limit.Pos(),
260 End: limit.Pos(),
261 NewText: []byte(beforeLimit),
262 },
263
264 {
265 Pos: limit.End(),
266 End: inc.End(),
267 },
268
269 {
270 Pos: limit.End(),
271 End: limit.End(),
272 NewText: []byte(afterLimit),
273 },
274 }...),
275 }},
276 })
277 }
278 }
279 }
280 }
281 }
282 return nil, nil
283 }
284
285
286
287
288
289
290 func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool {
291
292
293
294
295 cur := curId
296
297
298 ek, _ := cur.ParentEdge()
299 for ek == edge.ParenExpr_X {
300 cur = cur.Parent()
301 ek, _ = cur.ParentEdge()
302 }
303
304 switch ek {
305 case edge.AssignStmt_Lhs:
306 assign := cur.Parent().Node().(*ast.AssignStmt)
307 if assign.Tok != token.DEFINE {
308 return true
309 }
310 id := curId.Node().(*ast.Ident)
311 if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() {
312 return true
313 }
314 case edge.IncDecStmt_X:
315 return true
316 case edge.UnaryExpr_X:
317 if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
318 return true
319 }
320 }
321 return false
322 }
323
View as plain text