Repository: mgechev/revive (go)
Issue: nil panic on old go.mod (without `go` directive)
**Describe the bug**
```
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x71395e]

goroutine 1 [running]:
github.com/mgechev/revive/lint.detectGoMod({0xc0005b64e0, 0x27})
	/home/powerman/go/pkg/mod/github.com/mgechev/revive@v1.5.0/lint/linter.go:176 +0x1be
github.com/mgechev/revive/lint.(*Linter).Lint(0xc000456d10, {0xc000599740, 0x1, 0x10?}, {0xc0001d4200, 0x17, 0x20}, {0x0, 0x3fe999999999999a, {0x8b2c4b, ...}, ...})
	/home/powerman/go/pkg/mod/github.com/mgechev/revive@v1.5.0/lint/linter.go:94 +0x239
github.com/mgechev/revive/revivelib.(*Revive).Lint(0xc0000d1f20, {0xc000094148, 0x1, 0x0?})
	/home/powerman/go/pkg/mod/github.com/mgechev/revive@v1.5.0/revivelib/core.go:105 +0x43f
github.com/mgechev/revive/cli.RunRevive({0x0, 0x0, 0x0})
	/home/powerman/go/pkg/mod/github.com/mgechev/revive@v1.5.0/cli/main.go:64 +0x314
main.main()
	/home/powerman/go/pkg/mod/github.com/mgechev/revive@v1.5.0/main.go:7 +0x1a
```

**To Reproduce**
Steps to reproduce the behavior:
1. I updated revive `go install github.com/mgechev/revive@latest`
2. I run it with the following flags & configuration file:

```shell
mkdir revive-old-gomod-bug
cd revive-old-gomod-bug
echo module example.com/old > go.mod
echo 'package main; func main() {println("ok")}' > main.go
go run .
# Output:
# ok
revive ./...
# it panics
```

```toml
# no config
```

**Expected behavior**
It should not panic on outdated (but valid) projects.

**Desktop (please complete the following information):**
 - OS: Gentoo Linux
 - go version go1.23.2 linux/amd64

**Additional context**
`go mod tidy` adds `go` directive to `go.mod` and this fixes the panic.

Pull request description: fix: nil panic on old go.mod (issue #1100)
Closes #1100 


Patch:
diff --git a/go.mod b/go.mod
index 6f32071..347fd70 100644
--- a/go.mod
+++ b/go.mod
@@ -12,8 +12,8 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/afero v1.11.0
-	golang.org/x/mod v0.21.0
-	golang.org/x/tools v0.26.0
+	golang.org/x/mod v0.22.0
+	golang.org/x/tools v0.27.0
 )
 
 require (
@@ -22,6 +22,6 @@ require (
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/stretchr/testify v1.9.0 // indirect
-	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/sys v0.27.0 // indirect
 	golang.org/x/text v0.18.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 3d9ceb3..ef22115 100644
--- a/go.sum
+++ b/go.sum
@@ -44,18 +44,24 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
 golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
 golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
 golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
 golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
+golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/lint/linter.go b/lint/linter.go
index 56036e8..2bd8355 100644
--- a/lint/linter.go
+++ b/lint/linter.go
@@ -173,6 +173,10 @@ func detectGoMod(dir string) (rootDir string, ver *goversion.Version, err error)
 		return "", nil, fmt.Errorf("failed to parse %q, got %v", modFileName, err)
 	}
 
+	if modAst.Go == nil {
+		return "", nil, fmt.Errorf("%q does not specify a Go version", modFileName)
+	}
+
 	ver, err = goversion.NewVersion(modAst.Go.Version)
 	return filepath.Dir(modFileName), ver, err
 }


Test patch:
diff --git a/test/issue1100_test.go b/test/issue1100_test.go
new file mode 100644
index 0000000..4e3f9f6
--- /dev/null
+++ b/test/issue1100_test.go
@@ -0,0 +1,22 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestIssue1100(t *testing.T) {
+	args := []any{map[string]any{
+		"maxLitCount": "2",
+		"allowStrs":   "\"\"",
+		"allowInts":   "0,1,2",
+		"allowFloats": "0.0,1.0",
+		"ignoreFuncs": "os\\.(CreateFile|WriteFile|Chmod|FindProcess),\\.Println,ignoredFunc,\\.Info",
+	}}
+
+	testRule(t, "goUnknown/issue1100", &rule.AddConstantRule{}, &lint.RuleConfig{
+		Arguments: args,
+	})
+}
diff --git a/testdata/goUnknown/go.mod b/testdata/goUnknown/go.mod
new file mode 100644
index 0000000..33e314b
--- /dev/null
+++ b/testdata/goUnknown/go.mod
@@ -0,0 +1,2 @@
+module github.com/mgechev/revive/testdata
+
diff --git a/testdata/goUnknown/issue1100.go b/testdata/goUnknown/issue1100.go
new file mode 100644
index 0000000..527b33c
--- /dev/null
+++ b/testdata/goUnknown/issue1100.go
@@ -0,0 +1,104 @@
+package fixtures
+
+import (
+	"context"
+	"fmt"
+	"os"
+)
+
+type testLogger struct{}
+
+func (l *testLogger) Info(ctx context.Context, msg string) {}
+
+func getLogger() *testLogger {
+	return &testLogger{}
+}
+
+func test1007() {
+	getLogger().Info(context.Background(), "test1007")
+	getLogger().Info(context.Background(), "test1007")
+	getLogger().Info(context.Background(), "test1007")
+}
+
+func foo(a float32, b string, c any, d int) {
+	a = 1.0 // ignore
+	b = "ignore"
+	c = 2              // ignore
+	println("lit", 12) // MATCH /avoid magic numbers like '12', create a named constant for it/
+	if a == 12.50 {    // MATCH /avoid magic numbers like '12.50', create a named constant for it/
+		if b == "lit" {
+			c = "lit" // MATCH /string literal "lit" appears, at least, 3 times, create a named constant for it/
+		}
+		for i := 0; i < 1; i++ {
+			println("lit")
+		}
+	}
+
+	println(0666)           // MATCH /avoid magic numbers like '0666', create a named constant for it/
+	os.Chmod("test", 0666)  // ignore
+	os.FindProcess(102100)  // ignore
+	fmt.Println("test", 12) // ignore
+	fmt.Printf("%d", 100)   // MATCH /avoid magic numbers like '100', create a named constant for it/
+	myPrintln("%d", 100)    // MATCH /avoid magic numbers like '100', create a named constant for it/
+	ignoredFunc(1000)       // ignore
+	ignoredFunc1(1000)      // ignore - match regexp too
+
+	println("The result of calling myFunc is: ", ignoredFunc(100))             // ignore
+	println("result is: ", ignoredFunc(notIgnoredFunc(ignoredFunc(100))))      // ignore
+	println("result of calling myFunc is: ", notIgnoredFunc(ignoredFunc(100))) // ignore
+
+	println("result myFunc is: ", notIgnoredFunc(100))           // MATCH /avoid magic numbers like '100', create a named constant for it/
+	println("The result is: ", ignoredFunc(notIgnoredFunc(100))) // MATCH /avoid magic numbers like '100', create a named constant for it/
+}
+
+func myPrintln(s string, num int) {
+
+}
+
+func ignoredFunc1(num int) int {
+	return num
+}
+
+func ignoredFunc(num int) int {
+	return num
+}
+
+func notIgnoredFunc(num int) int {
+	return num
+}
+
+func tagsInStructLiteralsShouldBeOK() {
+	a := struct {
+		X int `json:"x"`
+	}{}
+
+	b := struct {
+		X int `json:"x"`
+	}{}
+
+	c := struct {
+		X int `json:"x"`
+	}{}
+
+	d := struct {
+		X int `json:"x"`
+		Y int `json:"y"`
+	}{}
+
+	e := struct {
+		X int `json:"x"`
+		Y int `json:"y"`
+	}{}
+
+	var f struct {
+		X int `json:"x"`
+		Y int `json:"y"`
+	}
+
+	var g struct {
+		X int `json:"x"`
+		Y int `json:"y"`
+	}
+
+	_, _, _, _, _, _, _ = a, b, c, d, e, f, g
+}