Repository: mgechev/revive

Issue:
Nil ast.Expr panic with 1.13.0 in golangci-lint 2.7.1
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**

Running `golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 --fix ./...` with `golangci-lint` 2.7.1, which seems to use revive 1.13.0, I see:

```
panic: interface conversion: ast.Expr is nil, not *ast.Ident

goroutine 1893802 [running]:
github.com/mgechev/revive/rule.(*lintInefficientMapLookup).analyzeBlock(0xc23d89a3b0, 0x1662280?)
        github.com/mgechev/revive@v1.13.0/rule/inefficient_map_lookup.go:88 +0x1d8
github.com/mgechev/revive/rule.(*lintInefficientMapLookup).Visit(0xc23d89a3b0, {0x1c1b260?, 0xc22d57b230?})
        github.com/mgechev/revive@v1.13.0/rule/inefficient_map_lookup.go:70 +0x32
go/ast.Walk({0x1c15d40?, 0xc23d89a3b0?}, {0x1c1b260, 0xc22d57b230})
        go/ast/walk.go:34 +0x4c
go/ast.Walk({0x1c15d40?, 0xc23d89a3b0?}, {0x1c1b710, 0xc23ff859c0})
        go/ast/walk.go:218 +0x2312
go/ast.walkList[...](...)
        go/ast/walk.go:21
go/ast.Walk({0x1c15d40?, 0xc23d89a3b0?}, {0x1c1b260, 0xc22d57b710})
        go/ast/walk.go:211 +0x1d6e
go/ast.Walk({0x1c15d40?, 0xc23d89a3b0?}, {0x1c1b828, 0xc2c3838000})
        go/ast/walk.go:272 +0x3307
go/ast.walkList[...](...)
        go/ast/walk.go:21
go/ast.Walk({0x1c15d40?, 0xc23d89a3b0?}, {0x1c1b260, 0xc22d57bb90})
        go/ast/walk.go:211 +0x1d6e
github.com/mgechev/revive/rule.(*InefficientMapLookupRule).Apply(0x150eb40?, 0xc0ecbd3140, {0x17720dd?, 0x16?, 0x56?})
        github.com/mgechev/revive@v1.13.0/rule/inefficient_map_lookup.go:47 +0x29b
github.com/mgechev/revive/lint.(*File).lint(0xc0ecbd3140, {0xc006141208, 0x4f, 0x8f}, {0x0, 0x3fe999999999999a, {0xc00612d05f, 0x7}, 0x1, 0x0, ...}, ...)
        github.com/mgechev/revive@v1.13.0/lint/file.go:126 +0x1e2
github.com/mgechev/revive/lint.(*Package).lint.func1()
        github.com/mgechev/revive@v1.13.0/lint/package.go:211 +0x5e
golang.org/x/sync/errgroup.(*Group).Go.func1()
        golang.org/x/sync@v0.18.0/errgroup/errgroup.go:93 +0x50
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1893797
        golang.org/x/sync@v0.18.0/errgroup/errgroup.go:78 +0x95
```

I have unfortunately not been able to narrow it down to a particular piece of code that I am able to share publicly yet.

**Expected behavior**

No panic.

**Desktop (please complete the following information):**
 - Ubuntu 25.10
 - Go 1.25.3


Discussion:
@oschwald: I am able to reproduce it with `revive` directly. I asked Claude Code to come up with a small test case.  Here is what it came up with. The code obviously doesn't seem like the right way to get a length, but it is valid.

## Minimal config (`revive.toml`)

```toml
[rule.inefficient-map-lookup]
```

## Minimal test case (`test.go`)

```go
package main

func count(m map[string]struct{}) int {
	var count int
	for range m {
		count++
	}
	return count
}

func main() {}
```

## Command to reproduce

```bash
revive -config revive.toml ./...
```
@chavacava: Hi @oschwald, thanks for reporting the issue and providing a test case.@chavacava: The PR #1604 should fix the problem.@oschwald: @chavacava thanks so much!

Solution patch:
diff --git a/rule/inefficient_map_lookup.go b/rule/inefficient_map_lookup.go
index b6e4bf9..c694420 100644
--- a/rule/inefficient_map_lookup.go
+++ b/rule/inefficient_map_lookup.go
@@ -155,6 +155,11 @@ func (w *lintInefficientMapLookup) isRangeOverMapKey(stmt ast.Stmt) bool {
 		return false // not a range
 	}
 
+	// Check if we range on the key
+	if rangeStmt.Key == nil {
+		return false // no key in range
+	}
+
 	// Check if we range only on key
 	// for key := range ...
 	// for key, _ := range ...



Test patch:
diff --git a/rule/redundant_test_main_exit.go b/rule/redundant_test_main_exit.go
index 717969e..cd1da59 100644
--- a/rule/redundant_test_main_exit.go
+++ b/rule/redundant_test_main_exit.go
@@ -65,6 +65,11 @@ func (w *lintRedundantTestMainExit) Visit(node ast.Node) ast.Visitor {
 	}
 
 	pkg := id.Name
+	// skip flag calls because they are commonly used in TestMain
+	if pkg == "flag" {
+		return w
+	}
+
 	fn := fc.Sel.Name
 	if isCallToExitFunction(pkg, fn, ce.Args) {
 		w.onFailure(lint.Failure{
diff --git a/testdata/go1.15/redundant_test_main_exit_test.go b/testdata/go1.15/redundant_test_main_exit_test.go
index 95dd88b..ddd339d 100644
--- a/testdata/go1.15/redundant_test_main_exit_test.go
+++ b/testdata/go1.15/redundant_test_main_exit_test.go
@@ -1,6 +1,7 @@
 package fixtures
 
 import (
+	"flag"
 	"fmt"
 	"os"
 	"syscall"
@@ -15,6 +16,26 @@ func TestMain(m *testing.M) {
 	syscall.Exit(i) // MATCH /redundant call to syscall.Exit in TestMain function, the test runner will handle it automatically as of Go 1.15/
 }
 
+func TestMain(m *testing.M) {
+	flag.Parse() // must not match
+	if testing.Short() {
+		m.Run()
+		return
+	}
+}
+
+var testBackend string
+
+func TestMain(m *testing.M) {
+	flag.StringVar(&testBackend, "backend", "", "selects backend")
+	flag.Parse() // must not match
+}
+
+func TestMain(m *testing.M) {
+	fs := flag.NewFlagSet("fs", flag.ExitOnError) // must not match
+	fs.Parse()
+}
+
 func setup() {
 	fmt.Println("Setup")
 }
diff --git a/testdata/inefficient_map_lookup.go b/testdata/inefficient_map_lookup.go
index 0c4b874..4ee100f 100644
--- a/testdata/inefficient_map_lookup.go
+++ b/testdata/inefficient_map_lookup.go
@@ -60,4 +60,11 @@ func inefficientMapLookup() {
 		}
 		fmt.Println(key)
 	}
+
+	// Test case for issue #1601
+	var count int
+	for range aMap {
+		count++
+	}
+	_ = count
 }