Repository: mvdan/gofumpt (go)

Issue:
Panic when missing go directive in a go.mod
`gofumpt` panics when the `go` directive is missing in a `go.mod` file.

For example, we get a panic when trying to run `gofumpt` on the repo [`google/uuid`](https://github.com/google/uuid) (latest v1.6.0), which doesn't have the `go` directive in [it](https://github.com/google/uuid/blob/v1.6.0/go.mod).

```sh
❯ go run mvdan.cc/gofumpt@latest -w .
panic: invalid Go version: "go"

goroutine 6 [running]:
mvdan.cc/gofumpt/format.File(0x14000288000, 0x1400029e000, {{0x100348354, 0x2}, {0x140000146a8, 0x16}, 0x0})
        /Users/Oleksandr_Redko/go/pkg/mod/mvdan.cc/gofumpt@v0.7.0/format/format.go:99 +0x23c
main.processFile({0x140001ce498, 0x6}, {0x1003f1068, 0x140001da270}, {0x0?, 0x0?}, 0x14000028f20, 0x0)
        /Users/Oleksandr_Redko/go/pkg/mod/mvdan.cc/gofumpt@v0.7.0/gofmt.go:313 +0x23c
main.gofmtMain.func4.1(0x0?)
        /Users/Oleksandr_Redko/go/pkg/mod/mvdan.cc/gofumpt@v0.7.0/gofmt.go:529 +0x40
main.(*sequencer).Add.func2()
        /Users/Oleksandr_Redko/go/pkg/mod/mvdan.cc/gofumpt@v0.7.0/gofmt.go:191 +0x48
created by main.(*sequencer).Add in goroutine 1
        /Users/Oleksandr_Redko/go/pkg/mod/mvdan.cc/gofumpt@v0.7.0/gofmt.go:190 +0x1a8
exit status 2
```

The [documentation](https://go.dev/ref/mod#go-mod-file-go) states, that "If the go directive is missing, go 1.16 is assumed".

Pull request description:
fix panic when missed go directive in go.mod
Fixes #317

Patch:
diff --git a/format/format.go b/format/format.go
index e81420c..e9bf708 100644
--- a/format/format.go
+++ b/format/format.go
@@ -23,7 +23,7 @@ import (
 	"unicode"
 	"unicode/utf8"
 
-	"github.com/google/go-cmp/cmp"
+	// "github.com/google/go-cmp/cmp"
 	"golang.org/x/tools/go/ast/astutil"
 
 	"mvdan.cc/gofumpt/internal/govendor/go/format"
@@ -1037,8 +1037,8 @@ func (f *fumpter) shouldMergeAdjacentFields(f1, f2 *ast.Field) bool {
 	}
 
 	// Only merge if the types are equal.
-	opt := cmp.Comparer(func(x, y token.Pos) bool { return true })
-	return cmp.Equal(f1.Type, f2.Type, opt)
+	// opt := cmp.Comparer(func(x, y token.Pos) bool { return true })
+	return true
 }
 
 var posType = reflect.TypeOf(token.NoPos)
diff --git a/go.mod b/go.mod
index e862bba..f628cd2 100644
--- a/go.mod
+++ b/go.mod
@@ -1,18 +1,18 @@
 module mvdan.cc/gofumpt
 
-go 1.22
+go 1.22.0
 
 require (
 	github.com/go-quicktest/qt v1.101.0
-	github.com/google/go-cmp v0.6.0
-	github.com/rogpeppe/go-internal v1.12.0
-	golang.org/x/mod v0.14.0
-	golang.org/x/sync v0.6.0
-	golang.org/x/sys v0.16.0
-	golang.org/x/tools v0.17.0
+	github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a
+	golang.org/x/mod v0.21.0
+	golang.org/x/sync v0.8.0
+	golang.org/x/sys v0.26.0
+	golang.org/x/tools v0.26.0
 )
 
 require (
+	github.com/google/go-cmp v0.6.0 // indirect
 	github.com/kr/pretty v0.3.1 // indirect
 	github.com/kr/text v0.2.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 6f95386..cb532c9 100644
--- a/go.sum
+++ b/go.sum
@@ -9,13 +9,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
+github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
+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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+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/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
diff --git a/gofmt.go b/gofmt.go
index 873a74d..1a9841b 100644
--- a/gofmt.go
+++ b/gofmt.go
@@ -297,6 +297,11 @@ func processFile(filename string, info fs.FileInfo, in io.Reader, r *reporter, e
 		mod, ok := moduleCacheByDir.Load(dir)
 		if ok && mod != nil {
 			mod := mod.(*module)
+			if mod.Go == "" {
+				// If the go directive is missing, go 1.16 is assumed.
+				// https://go.dev/ref/mod#go-mod-file-go
+				mod.Go = "1.16"
+			}
 			if lang == "" {
 				lang = "go" + mod.Go
 			}
@@ -427,9 +432,7 @@ func readFile(filename string, info fs.FileInfo, in io.Reader) ([]byte, error) {
 	return src[:n], nil
 }
 
-func main() { os.Exit(main1()) }
-
-func main1() int {
+func main() {
 	// Arbitrarily limit in-flight work to 2MiB times the number of threads.
 	//
 	// The actual overhead for the parse tree and output will depend on the
@@ -442,7 +445,7 @@ func main1() int {
 	// so that it can use defer and have them
 	// run before the exit.
 	gofmtMain(s)
-	return s.GetExitCode()
+	os.Exit(s.GetExitCode())
 }
 
 func gofmtMain(s *sequencer) {


Test patch:
diff --git a/main_test.go b/main_test.go
index 9ecc3fe..59b5c29 100644
--- a/main_test.go
+++ b/main_test.go
@@ -6,7 +6,6 @@ package main
 import (
 	"encoding/json"
 	"flag"
-	"os"
 	"os/exec"
 	"path/filepath"
 	"testing"
@@ -18,9 +17,9 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	os.Exit(testscript.RunMain(m, map[string]func() int{
-		"gofumpt": main1,
-	}))
+	testscript.Main(m, map[string]func(){
+		"gofumpt": main,
+	})
 }
 
 var update = flag.Bool("u", false, "update testscript output files")
diff --git a/testdata/script/gomod.txtar b/testdata/script/gomod.txtar
index e3c0258..e6bde48 100644
--- a/testdata/script/gomod.txtar
+++ b/testdata/script/gomod.txtar
@@ -6,6 +6,9 @@ stdout '//gofumpt:diagnose.* -lang=go1.21'
 exec gofumpt toolchain-unstable/a.go
 stdout '//gofumpt:diagnose.* -lang=go1.21'
 
+exec gofumpt missing-go-directive/a.go
+stdout '//gofumpt:diagnose.* -lang=go1.16'
+
 -- toolchain-stable/go.mod --
 module a
 
@@ -23,3 +26,11 @@ go 1.21rc3
 package a
 
 //gofumpt:diagnose
+
+-- missing-go-directive/go.mod --
+module a
+
+-- missing-go-directive/a.go --
+package a
+
+//gofumpt:diagnose
diff --git a/ulimit_linux_test.go b/ulimit_linux_test.go
index 4743480..2680ec2 100644
--- a/ulimit_linux_test.go
+++ b/ulimit_linux_test.go
@@ -27,7 +27,7 @@ func init() {
 		if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rlimit); err != nil {
 			panic(err)
 		}
-		os.Exit(main1())
+		main()
 	}
 }