Skip to content

Commit 11a3153

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/analysis/modernize: rangeint: respect side effects
This CL fixes a serious bug in rangeint that caused it to offer a fix from "for i := 0; i < len(s); i++" to "for range s" even when the loop may modify the value of slice s. (The for loop reads the length on each iteration, whereas the range loop reads it only once.) + test Fixes golang/go#72917 Change-Id: Id0c926f0590241736c7fe7589c2796a075d05744 Reviewed-on: https://go-review.googlesource.com/c/tools/+/660435 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent 19f73a6 commit 11a3153

File tree

3 files changed

+151
-19
lines changed

3 files changed

+151
-19
lines changed

gopls/internal/analysis/modernize/rangeint.go

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import (
1515
"golang.org/x/tools/go/ast/inspector"
1616
"golang.org/x/tools/go/types/typeutil"
1717
"golang.org/x/tools/internal/analysisinternal"
18+
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
1819
"golang.org/x/tools/internal/astutil/cursor"
1920
"golang.org/x/tools/internal/astutil/edge"
21+
"golang.org/x/tools/internal/typesinternal"
22+
"golang.org/x/tools/internal/typesinternal/typeindex"
2023
)
2124

2225
// rangeint offers a fix to replace a 3-clause 'for' loop:
@@ -38,13 +41,23 @@ import (
3841
// - The limit must not be b.N, to avoid redundancy with bloop's fixes.
3942
//
4043
// 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.
4455
func rangeint(pass *analysis.Pass) {
4556
info := pass.TypesInfo
4657

4758
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
59+
typeindex := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
60+
4861
for curFile := range filesUsing(inspect, info, "go1.22") {
4962
nextLoop:
5063
for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
@@ -62,19 +75,39 @@ func rangeint(pass *analysis.Pass) {
6275
// Have: for i = 0; i < limit; ... {}
6376

6477
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]
7188
}
7289

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
78111
}
79112

80113
if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
@@ -93,7 +126,7 @@ func rangeint(pass *analysis.Pass) {
93126
// Reject if any is an l-value (assigned or address-taken):
94127
// a "for range int" loop does not respect assignments to
95128
// the loop variable.
96-
if isScalarLvalue(curId) {
129+
if isScalarLvalue(info, curId) {
97130
continue nextLoop
98131
}
99132
}
@@ -213,7 +246,7 @@ func rangeint(pass *analysis.Pass) {
213246
//
214247
// This function is valid only for scalars (x = ...),
215248
// not for aggregates (x.a[i] = ...)
216-
func isScalarLvalue(curId cursor.Cursor) bool {
249+
func isScalarLvalue(info *types.Info, curId cursor.Cursor) bool {
217250
// Unfortunately we can't simply use info.Types[e].Assignable()
218251
// as it is always true for a variable even when that variable is
219252
// used only as an r-value. So we must inspect enclosing syntax.
@@ -229,7 +262,14 @@ func isScalarLvalue(curId cursor.Cursor) bool {
229262

230263
switch ek {
231264
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+
}
233273
case edge.IncDecStmt_X:
234274
return true // i++, i--
235275
case edge.UnaryExpr_X:

gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func _(i int, s struct{ i int }, slice []int) {
4848

4949
{
5050
var i int
51-
for i = 0; i < f(); i++ { // want "for loop can be modernized using range over int"
51+
for i = 0; i < 10; i++ { // want "for loop can be modernized using range over int"
5252
}
5353
// NB: no uses of i after loop.
5454
}
@@ -90,8 +90,54 @@ func _(i int, s struct{ i int }, slice []int) {
9090
for i := 0; i < 10; i++ { // nope: assigns i
9191
i = 8
9292
}
93+
94+
// The limit expression must be loop invariant;
95+
// see https://github.com/golang/go/issues/72917
96+
for i := 0; i < f(); i++ { // nope
97+
}
98+
{
99+
var s struct{ limit int }
100+
for i := 0; i < s.limit; i++ { // nope: limit is not a const or local var
101+
}
102+
}
103+
{
104+
const k = 10
105+
for i := 0; i < k; i++ { // want "for loop can be modernized using range over int"
106+
}
107+
}
108+
{
109+
var limit = 10
110+
for i := 0; i < limit; i++ { // want "for loop can be modernized using range over int"
111+
}
112+
}
113+
{
114+
var limit = 10
115+
for i := 0; i < limit; i++ { // nope: limit is address-taken
116+
}
117+
print(&limit)
118+
}
119+
{
120+
limit := 10
121+
limit++
122+
for i := 0; i < limit; i++ { // nope: limit is assigned other than by its declaration
123+
}
124+
}
125+
for i := 0; i < Global; i++ { // nope: limit is an exported global var; may be updated elsewhere
126+
}
127+
for i := 0; i < len(table); i++ { // want "for loop can be modernized using range over int"
128+
}
129+
{
130+
s := []string{}
131+
for i := 0; i < len(s); i++ { // nope: limit is not loop-invariant
132+
s = s[1:]
133+
}
134+
}
93135
}
94136

137+
var Global int
138+
139+
var table = []string{"hello", "world"}
140+
95141
func f() int { return 0 }
96142

97143
// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"):

gopls/internal/analysis/modernize/testdata/src/rangeint/rangeint.go.golden

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func _(i int, s struct{ i int }, slice []int) {
4848

4949
{
5050
var i int
51-
for i = range f() { // want "for loop can be modernized using range over int"
51+
for i = range 10 { // want "for loop can be modernized using range over int"
5252
}
5353
// NB: no uses of i after loop.
5454
}
@@ -90,8 +90,54 @@ func _(i int, s struct{ i int }, slice []int) {
9090
for i := 0; i < 10; i++ { // nope: assigns i
9191
i = 8
9292
}
93+
94+
// The limit expression must be loop invariant;
95+
// see https://github.com/golang/go/issues/72917
96+
for i := 0; i < f(); i++ { // nope
97+
}
98+
{
99+
var s struct{ limit int }
100+
for i := 0; i < s.limit; i++ { // nope: limit is not a const or local var
101+
}
102+
}
103+
{
104+
const k = 10
105+
for range k { // want "for loop can be modernized using range over int"
106+
}
107+
}
108+
{
109+
var limit = 10
110+
for range limit { // want "for loop can be modernized using range over int"
111+
}
112+
}
113+
{
114+
var limit = 10
115+
for i := 0; i < limit; i++ { // nope: limit is address-taken
116+
}
117+
print(&limit)
118+
}
119+
{
120+
limit := 10
121+
limit++
122+
for i := 0; i < limit; i++ { // nope: limit is assigned other than by its declaration
123+
}
124+
}
125+
for i := 0; i < Global; i++ { // nope: limit is an exported global var; may be updated elsewhere
126+
}
127+
for range table { // want "for loop can be modernized using range over int"
128+
}
129+
{
130+
s := []string{}
131+
for i := 0; i < len(s); i++ { // nope: limit is not loop-invariant
132+
s = s[1:]
133+
}
134+
}
93135
}
94136

137+
var Global int
138+
139+
var table = []string{"hello", "world"}
140+
95141
func f() int { return 0 }
96142

97143
// Repro for part of #71847: ("for range n is invalid if the loop body contains i++"):

0 commit comments

Comments
 (0)