go/analysis/passes/modernize: rangeint: omit unneeded type cast
cl/653616 introduced a type conversion on the limit of a "for range"
if the limit is not assignable to an int. However, this is not
necessary if the init value is created outside of the loop and
if the default value of the limit is an integer because the compiler
can infer the type of the iteration values without the type cast.
Fixes golang/go#77691
diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go
index 257f9e9..3a666f7 100644
--- a/go/analysis/passes/modernize/rangeint.go
+++ b/go/analysis/passes/modernize/rangeint.go
@@ -230,7 +230,7 @@
// such as "const limit = 1e3", its effective type may
// differ between the two forms.
// In a for loop, it must be comparable with int i,
- // for i := 0; i < limit; i++
+ // for i := 0; i < limit; i++ {}
// but in a range loop it would become a float,
// for i := range limit {}
// which is a type error. We need to convert it to int
@@ -253,6 +253,19 @@
if types.AssignableTo(tLimit, tVar) {
beforeLimit, afterLimit = "", ""
}
+ // Check if the limit's default type is an integer.
+ // If it is, and we are assigning to an existing variable, the compiler
+ // handles the type context automatically, so we don't need a cast.
+ // e.g.
+ // var i int64
+ // for i = 0; i < 10; i ++ {} --> for i = range 10 {}
+ // This also prevents us from adding a cast that would break non-target builds,
+ // if "i" uses an architecture-dependent type such as syscall.Timespec.Nsec.
+ if init.Tok == token.ASSIGN { // init is assigning to existing var
+ if b, ok := tLimit.Underlying().(*types.Basic); ok && b.Info()&types.IsInteger != 0 {
+ beforeLimit, afterLimit = "", ""
+ }
+ }
}
}
diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go
index 0ec4374..567a962 100644
--- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go
+++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go
@@ -324,3 +324,30 @@
}
}
}
+
+type C int32
+
+// Don't add an unneeded type cast if the limit's default type is an integer, and
+// the init value is assigned outside the loop, because the compiler can infer the type.
+func issue77891() {
+ const limit_float = 1e6
+ var i int
+ for i = 0; i < limit_float; i++ { // want "for loop can be modernized using range over int"
+ println(i)
+ }
+
+ var j int64
+ for j = 0; j < 20; j++ { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+
+ var c C
+ for c = 0; c < 10; c++ { // want "for loop can be modernized using range over int"
+ println(c)
+ }
+
+ var ptr uintptr
+ for ptr = 0; ptr < 100; ptr++ { // want "for loop can be modernized using range over int"
+ println(ptr)
+ }
+}
diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden
index ad98069..e02731d 100644
--- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden
+++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden
@@ -323,3 +323,30 @@
}
}
}
+
+type C int32
+
+// Don't add an unneeded type cast if the limit's default type is an integer, and
+// the init value is assigned outside the loop, because the compiler can infer the type.
+func issue77891() {
+ const limit_float = 1e6
+ var i int
+ for i = range int(limit_float) { // want "for loop can be modernized using range over int"
+ println(i)
+ }
+
+ var j int64
+ for j = range 20 { // want "for loop can be modernized using range over int"
+ println(j)
+ }
+
+ var c C
+ for c = range 10 { // want "for loop can be modernized using range over int"
+ println(c)
+ }
+
+ var ptr uintptr
+ for ptr = range 100 { // want "for loop can be modernized using range over int"
+ println(ptr)
+ }
+}
\ No newline at end of file
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
cl/653616 introduced a type conversion on the limit of a "for range""cl/" relies on Google's corp network, and is not marked up by Gerrit as a link. If you use "CL 653616", Gerrit will linkify it. Alternatively, use https://go.dev/cl/653616 anywhere.
For issues, if you use #XXX (main repo) or golang/go#XXX (x/tools) in CL descriptions (etc) then both Gerrit and GitHub will mark these up as links. Alternatively you can use https://go.dev/issue/XXX anywhere.
// If it is, and we are assigning to an existing variable, the compiler
// handles the type context automatically, so we don't need a cast.I see that this change makes the new test pass, but why is the logic below not a special case of the existing AssignableTo(untyped float, int64) check, which returns true?
// This also prevents us from adding a cast that would break non-target builds,"Cast" is a term from C; in Go we call it a conversion.
This might be clearer as:
```
// Redundant casts are not only unsightly but may in some cases cause
// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
// into otherwise portable files.
```
if b, ok := tLimit.Underlying().(*types.Basic); ok && b.Info()&types.IsInteger != 0 {Let's pull this local function out of `unsafefuncs` to package level, and use it again here:
```
func isInteger(t types.Type) bool {
basic, ok := t.Underlying().(*types.Basic)
return ok && basic.Info()&types.IsInteger != 0
}
```
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
cl/653616 introduced a type conversion on the limit of a "for range""cl/" relies on Google's corp network, and is not marked up by Gerrit as a link. If you use "CL 653616", Gerrit will linkify it. Alternatively, use https://go.dev/cl/653616 anywhere.
For issues, if you use #XXX (main repo) or golang/go#XXX (x/tools) in CL descriptions (etc) then both Gerrit and GitHub will mark these up as links. Alternatively you can use https://go.dev/issue/XXX anywhere.
Thanks! Done
// If it is, and we are assigning to an existing variable, the compiler
// handles the type context automatically, so we don't need a cast.I see that this change makes the new test pass, but why is the logic below not a special case of the existing AssignableTo(untyped float, int64) check, which returns true?
We are passing types.Default, so the types.Type passed to AssignableTo is typed, not untyped. So I don't believe this logic is a special case of the AssignableTo check (unless we change it to check AssignableTo on the "raw" type, which I'm unsure of).
// This also prevents us from adding a cast that would break non-target builds,"Cast" is a term from C; in Go we call it a conversion.
This might be clearer as:
```
// Redundant casts are not only unsightly but may in some cases cause
// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
// into otherwise portable files.
```
Done
if b, ok := tLimit.Underlying().(*types.Basic); ok && b.Info()&types.IsInteger != 0 {Let's pull this local function out of `unsafefuncs` to package level, and use it again here:
```
func isInteger(t types.Type) bool {
basic, ok := t.Underlying().(*types.Basic)
return ok && basic.Info()&types.IsInteger != 0
}
```
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
// If it is, and we are assigning to an existing variable, the compiler
// handles the type context automatically, so we don't need a cast.Madeline KalilI see that this change makes the new test pass, but why is the logic below not a special case of the existing AssignableTo(untyped float, int64) check, which returns true?
We are passing types.Default, so the types.Type passed to AssignableTo is typed, not untyped. So I don't believe this logic is a special case of the AssignableTo check (unless we change it to check AssignableTo on the "raw" type, which I'm unsure of).
Still I would have expected AssignableTo to work for untyped types.
...
Ah, the problem is that removing Default causes us to check (say, for a limit of `1e6`) whether untyped float is assignable to int: it is... but we should always keep the conversion if the limit is non-integer. That leads to a more natural factoring:
```
tLimit := info2.TypeOf(limit)
// Eliminate conversion when safe.
//
// Redundant conversions are not only unsightly but may in some cases cause
// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
// into otherwise portable files.
//
// The operand must have an integer type (not, say, '1e6')
// even when assigning to an existing integer variable.
if isInteger(tLimit) {
// When declaring a new var from an untyped limit,
// the limit's default type is what matters.
if init.Tok != token.ASSIGN {
tLimit = types.Default(tLimit)
}
if types.AssignableTo(tLimit, tVar) {
beforeLimit, afterLimit = "", ""
}
}
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Auto-Submit | +1 |
| Commit-Queue | +1 |
// If it is, and we are assigning to an existing variable, the compiler
// handles the type context automatically, so we don't need a cast.Madeline KalilI see that this change makes the new test pass, but why is the logic below not a special case of the existing AssignableTo(untyped float, int64) check, which returns true?
Alan DonovanWe are passing types.Default, so the types.Type passed to AssignableTo is typed, not untyped. So I don't believe this logic is a special case of the AssignableTo check (unless we change it to check AssignableTo on the "raw" type, which I'm unsure of).
Still I would have expected AssignableTo to work for untyped types.
...
Ah, the problem is that removing Default causes us to check (say, for a limit of `1e6`) whether untyped float is assignable to int: it is... but we should always keep the conversion if the limit is non-integer. That leads to a more natural factoring:```
tLimit := info2.TypeOf(limit)// Eliminate conversion when safe.
//
// Redundant conversions are not only unsightly but may in some cases cause
// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
// into otherwise portable files.
//
// The operand must have an integer type (not, say, '1e6')
// even when assigning to an existing integer variable.
if isInteger(tLimit) {
// When declaring a new var from an untyped limit,
// the limit's default type is what matters.
if init.Tok != token.ASSIGN {
tLimit = types.Default(tLimit)
}
if types.AssignableTo(tLimit, tVar) {
beforeLimit, afterLimit = "", ""
}
}
```
Makes sense, thanks
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
3 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: go/analysis/passes/modernize/rangeint.go
Insertions: 15, Deletions: 12.
@@ -249,21 +249,24 @@
beforeLimit, afterLimit = fmt.Sprintf("%s(", types.TypeString(tVar, qual)), ")"
info2 := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
if types.CheckExpr(pass.Fset, pass.Pkg, limit.Pos(), limit, info2) == nil {
- tLimit := types.Default(info2.TypeOf(limit))
- if types.AssignableTo(tLimit, tVar) {
- beforeLimit, afterLimit = "", ""
- }
- // Check if the limit's default type is an integer.
- // If it is, and we are assigning to an existing variable, the compiler
- // handles the type context automatically, so we don't need a cast.
- // e.g.
- // var i int64
- // for i = 0; i < 10; i ++ {} --> for i = range 10 {}
+ tLimit := info2.TypeOf(limit)
+ // Eliminate conversion when safe.
+ //
// Redundant conversions are not only unsightly but may in some cases cause
// architecture-specific types (e.g. syscall.Timespec.Nsec) to be inserted
// into otherwise portable files.
- if init.Tok == token.ASSIGN && isInteger(tLimit) { // init is assigning to existing var
- beforeLimit, afterLimit = "", ""
+ //
+ // The operand must have an integer type (not, say, '1e6')
+ // even when assigning to an existing integer variable.
+ if isInteger(tLimit) {
+ // When declaring a new var from an untyped limit,
+ // the limit's default type is what matters.
+ if init.Tok != token.ASSIGN {
+ tLimit = types.Default(tLimit)
+ }
+ if types.AssignableTo(tLimit, tVar) {
+ beforeLimit, afterLimit = "", ""
+ }
}
}
}
```
go/analysis/passes/modernize: rangeint: omit unneeded type conversion
CL 653616 introduced a type conversion on the limit of a "for range"
if the limit is not assignable to an int. However, this is not
necessary if the init value is created outside of the loop and
if the default value of the limit is an integer because the compiler
can infer the type of the iteration values without the type conversion.
A type conversion can cause build issues if the loop's index uses
an architecture-dependent type such as syscall.Timespec.Nsec.
Fixes golang/go#77691
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |