Skip to content

x/tools/gopls/internal/analysis/modernize: rangeint: transformation unsound when loop variable is defined outside loop #73009

@riannucci

Description

@riannucci

gopls version

Build info
----------
golang.org/x/tools/gopls v0.18.1
    golang.org/x/tools/gopls@v0.18.1 h1:2xJBNzdImS5u/kV/ZzqDLSvlBSeZX+pWY9uKVP7Pask=
    github.com/BurntSushi/toml@v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
    github.com/google/go-cmp@v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
    golang.org/x/exp/typeparams@v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0=
    golang.org/x/mod@v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
    golang.org/x/sync@v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
    golang.org/x/telemetry@v0.0.0-20241220003058-cc96b6e0d3d9 h1:L2k9GUV2TpQKVRGMjN94qfUMgUwOFimSQ6gipyJIjKw=
    golang.org/x/text@v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
    golang.org/x/tools@v0.30.1-0.20250221230316-5055f70f240c h1:Ja/5gV5a9Vvho3p2NC/T2TtxhHjrWS/2DvCKMvA0a+Y=
    golang.org/x/vuln@v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw=
    honnef.co/go/tools@v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I=
    mvdan.cc/gofumpt@v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
    mvdan.cc/xurls/v2@v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
go: go1.23.6

go env

Not applicable

What did you do?

Given a main.go:

package main

// longestPrefix finds the length of the shared prefix
// of two strings
func longestPrefix(k1, k2 string) int {
	var i int
	for i = 0; i < min(len(k1), len(k2)); i++ {
		if k1[i] != k2[i] {
			break
		}
	}
	return i
}

func main() {
	if longestPrefix("hello", "h") != 1 {
		panic("madness!!")
	}
}

You can run:

$ go run main.go
$ # OK
$ modernize -fix main.go
$ # loop now looks like "for i = range min(len(k1), len(k2)) {" with all other lines unchanged
$ go run main.go
panic: madness!!

goroutine 1 [running]:
main.main()
	/Users/username/main.go:17 +0x5c
exit status 2
$

(I encountered this while converting some old Go code - I accepted modernize's suggestion without thinking about it, but eventually traced the failing acceptance tests back to this transformation)

What did you see happen?

Modernize rewrote the loop to use range over int, except that this changes the loop's intended effect on the loop variable. Normally it would have had an additional increment in order to fail the i < min(len(k1), len(k2)) comparison, but that no longer happens.

I saw #72917 but this seemed subtlely different enough (and likely easy to recognize with a local syntax matcher vs looking at the loop body)

What did you expect to see?

Modernize should have left this loop alone (or - range over int should correctly affect the loop variable in this case, but not sure that this is feasible).

Editor and settings

Not applicable

Logs

Not applicable

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.ToolsThis label describes issues relating to any tools in the x/tools repository.goplsIssues related to the Go language server, gopls.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions