@@ -15,8 +15,11 @@ import (
15
15
"golang.org/x/tools/go/ast/inspector"
16
16
"golang.org/x/tools/go/types/typeutil"
17
17
"golang.org/x/tools/internal/analysisinternal"
18
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
18
19
"golang.org/x/tools/internal/astutil/cursor"
19
20
"golang.org/x/tools/internal/astutil/edge"
21
+ "golang.org/x/tools/internal/typesinternal"
22
+ "golang.org/x/tools/internal/typesinternal/typeindex"
20
23
)
21
24
22
25
// rangeint offers a fix to replace a 3-clause 'for' loop:
@@ -38,13 +41,23 @@ import (
38
41
// - The limit must not be b.N, to avoid redundancy with bloop's fixes.
39
42
//
40
43
// Caveats:
41
- // - The fix will cause the limit expression to be evaluated exactly
42
- // once, instead of once per iteration. The limit may be a function call
43
- // (e.g. seq.Len()). The fix may change the cardinality of side effects.
44
+ //
45
+ // The fix causes the limit expression to be evaluated exactly once,
46
+ // instead of once per iteration. So, to avoid changing the
47
+ // cardinality of side effects, the limit expression must not involve
48
+ // function calls (e.g. seq.Len()) or channel receives. Moreover, the
49
+ // value of the limit expression must be loop invariant, which in
50
+ // practice means it must take one of the following forms:
51
+ //
52
+ // - a local variable that is assigned only once and not address-taken;
53
+ // - a constant; or
54
+ // - len(s), where s has the above properties.
44
55
func rangeint (pass * analysis.Pass ) {
45
56
info := pass .TypesInfo
46
57
47
58
inspect := pass .ResultOf [inspect .Analyzer ].(* inspector.Inspector )
59
+ typeindex := pass .ResultOf [typeindexanalyzer .Analyzer ].(* typeindex.Index )
60
+
48
61
for curFile := range filesUsing (inspect , info , "go1.22" ) {
49
62
nextLoop:
50
63
for curLoop := range curFile .Preorder ((* ast .ForStmt )(nil )) {
@@ -62,19 +75,39 @@ func rangeint(pass *analysis.Pass) {
62
75
// Have: for i = 0; i < limit; ... {}
63
76
64
77
limit := compare .Y
65
- curLimit , _ := curLoop .FindNode (limit )
66
- // Don't offer a fix if the limit expression depends on the loop index.
67
- for cur := range curLimit .Preorder ((* ast .Ident )(nil )) {
68
- if cur .Node ().(* ast.Ident ).Name == index .Name {
69
- continue nextLoop
70
- }
78
+
79
+ // If limit is "len(slice)", simplify it to "slice".
80
+ //
81
+ // (Don't replace "for i := 0; i < len(map); i++"
82
+ // with "for range m" because it's too hard to prove
83
+ // that len(m) is loop-invariant).
84
+ if call , ok := limit .(* ast.CallExpr ); ok &&
85
+ typeutil .Callee (info , call ) == builtinLen &&
86
+ is [* types.Slice ](info .TypeOf (call .Args [0 ]).Underlying ()) {
87
+ limit = call .Args [0 ]
71
88
}
72
89
73
- // Skip loops up to b.N in benchmarks; see [bloop].
74
- if sel , ok := limit .(* ast.SelectorExpr ); ok &&
75
- sel .Sel .Name == "N" &&
76
- analysisinternal .IsPointerToNamed (info .TypeOf (sel .X ), "testing" , "B" ) {
77
- continue // skip b.N
90
+ // Check the form of limit: must be a constant,
91
+ // or a local var that is not assigned or address-taken.
92
+ limitOK := false
93
+ if info .Types [limit ].Value != nil {
94
+ limitOK = true // constant
95
+ } else if id , ok := limit .(* ast.Ident ); ok {
96
+ if v , ok := info .Uses [id ].(* types.Var ); ok &&
97
+ ! (v .Exported () && typesinternal .IsPackageLevel (v )) {
98
+ // limit is a local or unexported global var.
99
+ // (An exported global may have uses we can't see.)
100
+ for cur := range typeindex .Uses (v ) {
101
+ if isScalarLvalue (info , cur ) {
102
+ // Limit var is assigned or address-taken.
103
+ continue nextLoop
104
+ }
105
+ }
106
+ limitOK = true
107
+ }
108
+ }
109
+ if ! limitOK {
110
+ continue nextLoop
78
111
}
79
112
80
113
if inc , ok := loop .Post .(* ast.IncDecStmt ); ok &&
@@ -93,7 +126,7 @@ func rangeint(pass *analysis.Pass) {
93
126
// Reject if any is an l-value (assigned or address-taken):
94
127
// a "for range int" loop does not respect assignments to
95
128
// the loop variable.
96
- if isScalarLvalue (curId ) {
129
+ if isScalarLvalue (info , curId ) {
97
130
continue nextLoop
98
131
}
99
132
}
@@ -213,7 +246,7 @@ func rangeint(pass *analysis.Pass) {
213
246
//
214
247
// This function is valid only for scalars (x = ...),
215
248
// not for aggregates (x.a[i] = ...)
216
- func isScalarLvalue (curId cursor.Cursor ) bool {
249
+ func isScalarLvalue (info * types. Info , curId cursor.Cursor ) bool {
217
250
// Unfortunately we can't simply use info.Types[e].Assignable()
218
251
// as it is always true for a variable even when that variable is
219
252
// used only as an r-value. So we must inspect enclosing syntax.
@@ -229,7 +262,14 @@ func isScalarLvalue(curId cursor.Cursor) bool {
229
262
230
263
switch ek {
231
264
case edge .AssignStmt_Lhs :
232
- return true // i = j
265
+ assign := cur .Parent ().Node ().(* ast.AssignStmt )
266
+ if assign .Tok == token .ASSIGN {
267
+ return true // i = j
268
+ }
269
+ id := curId .Node ().(* ast.Ident )
270
+ if v , ok := info .Defs [id ]; ok && v .Pos () != id .Pos () {
271
+ return true // reassignment of i (i, j := 1, 2)
272
+ }
233
273
case edge .IncDecStmt_X :
234
274
return true // i++, i--
235
275
case edge .UnaryExpr_X :
0 commit comments