modfile: be less strict about go version syntax in dependency go.mod files

It is unclear what the future holds for the go line in go.mod files.
Perhaps at some point we will switch to semver numbering.
Perhaps at some point we will allow specifying minor versions
or even betas and release candidates.
Those kinds of changes are difficult today because the go line
is parsed in dependency modules, meaning that older
versions of the Go toolchain need to understand newer go lines.

This CL makes that case - parsing a go line in a dependency's
go.mod file - a bit more lax about how to find the version.
It allows a leading v and any trailing non-digit-prefixed string
after the MAJOR.MINOR section.

There are no concrete plans to make use of any of these changes,
but if in the future we want to make them, having a few Go releases
under out belt that will accept the syntax in dependencies will
make any changes significantly easier.

See also CL 317690 in the main repo.

Change-Id: I7c7733c62259b3f25683ed618bc4918c98061396
Reviewed-on: https://go-review.googlesource.com/c/mod/+/317689
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/modfile/read_test.go b/modfile/read_test.go
index 7065e91..fea0d5d 100644
--- a/modfile/read_test.go
+++ b/modfile/read_test.go
@@ -424,9 +424,10 @@
 }
 
 func TestGoVersion(t *testing.T) {
-	for _, test := range []struct {
+	tests := []struct {
 		desc, input string
 		ok          bool
+		laxOK       bool // ok=true implies laxOK=true; only set if ok=false
 	}{
 		{desc: "empty", input: "module m\ngo \n", ok: false},
 		{desc: "one", input: "module m\ngo 1\n", ok: false},
@@ -435,15 +436,38 @@
 		{desc: "before", input: "module m\ngo v1.2\n", ok: false},
 		{desc: "after", input: "module m\ngo 1.2rc1\n", ok: false},
 		{desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false},
-	} {
-		t.Run(test.desc, func(t *testing.T) {
-			if _, err := Parse("go.mod", []byte(test.input), nil); err == nil && !test.ok {
-				t.Error("unexpected success")
-			} else if err != nil && test.ok {
-				t.Errorf("unexpected error: %v", err)
-			}
-		})
+		{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: false, laxOK: true},
+		{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: false, laxOK: true},
+		{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: false, laxOK: true},
+		{desc: "alt4", input: "module m\ngo 1.2.beta1\n", ok: false, laxOK: true},
+		{desc: "alt1", input: "module m\ngo v1.2.3\n", ok: false, laxOK: true},
+		{desc: "alt2", input: "module m\ngo v1.2rc1\n", ok: false, laxOK: true},
+		{desc: "alt3", input: "module m\ngo v1.2beta1\n", ok: false, laxOK: true},
+		{desc: "alt4", input: "module m\ngo v1.2.beta1\n", ok: false, laxOK: true},
+		{desc: "alt1", input: "module m\ngo v1.2\n", ok: false, laxOK: true},
 	}
+	t.Run("Strict", func(t *testing.T) {
+		for _, test := range tests {
+			t.Run(test.desc, func(t *testing.T) {
+				if _, err := Parse("go.mod", []byte(test.input), nil); err == nil && !test.ok {
+					t.Error("unexpected success")
+				} else if err != nil && test.ok {
+					t.Errorf("unexpected error: %v", err)
+				}
+			})
+		}
+	})
+	t.Run("Lax", func(t *testing.T) {
+		for _, test := range tests {
+			t.Run(test.desc, func(t *testing.T) {
+				if _, err := Parse("go.mod", []byte(test.input), nil); err == nil && !(test.ok || test.laxOK) {
+					t.Error("unexpected success")
+				} else if err != nil && test.ok {
+					t.Errorf("unexpected error: %v", err)
+				}
+			})
+		}
+	})
 }
 
 func TestComments(t *testing.T) {
diff --git a/modfile/rule.go b/modfile/rule.go
index d8242de..7299e15 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -217,6 +217,7 @@
 }
 
 var GoVersionRE = lazyregexp.New(`^([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
+var laxGoVersionRE = lazyregexp.New(`^v?(([1-9][0-9]*)\.(0|[1-9][0-9]*))([^0-9].*)$`)
 
 func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, args []string, fix VersionFixer, strict bool) {
 	// If strict is false, this module is a dependency.
@@ -267,8 +268,17 @@
 			errorf("go directive expects exactly one argument")
 			return
 		} else if !GoVersionRE.MatchString(args[0]) {
-			errorf("invalid go version '%s': must match format 1.23", args[0])
-			return
+			fixed := false
+			if !strict {
+				if m := laxGoVersionRE.FindStringSubmatch(args[0]); m != nil {
+					args[0] = m[1]
+					fixed = true
+				}
+			}
+			if !fixed {
+				errorf("invalid go version '%s': must match format 1.23", args[0])
+				return
+			}
 		}
 
 		f.Go = &Go{Syntax: line}