modfile: add parsing support for toolchain

Add new toolchain directive to go.mod and go.work parser.
Also fix error checking in parsing tests.

For golang/go#57001.

Change-Id: Ib7603f82cbd667f2152ed6b0c5989f08c28ceb1c
Reviewed-on: https://go-review.googlesource.com/c/mod/+/497399
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
diff --git a/modfile/read_test.go b/modfile/read_test.go
index df4f117..7017ee1 100644
--- a/modfile/read_test.go
+++ b/modfile/read_test.go
@@ -181,7 +181,13 @@
 			pf1, err := Parse(base, data, nil)
 			if err != nil {
 				switch base {
-				case "testdata/replace2.in", "testdata/gopkg.in.golden":
+				case "testdata/block.golden",
+					"testdata/block.in",
+					"testdata/comment.golden",
+					"testdata/comment.in",
+					"testdata/rule1.golden":
+					// ignore
+				default:
 					t.Errorf("should parse %v: %v", base, err)
 				}
 			}
@@ -425,12 +431,13 @@
 	}
 }
 
-func TestGoVersion(t *testing.T) {
+func TestParseVersions(t *testing.T) {
 	tests := []struct {
 		desc, input string
 		ok          bool
 		laxOK       bool // ok=true implies laxOK=true; only set if ok=false
 	}{
+		// go lines
 		{desc: "empty", input: "module m\ngo \n", ok: false},
 		{desc: "one", input: "module m\ngo 1\n", ok: false},
 		{desc: "two", input: "module m\ngo 1.22\n", ok: true},
@@ -438,15 +445,23 @@
 		{desc: "before", input: "module m\ngo v1.2\n", ok: false},
 		{desc: "after", input: "module m\ngo 1.2rc1\n", ok: true},
 		{desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false},
-		{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: true, laxOK: true},
-		{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: true, laxOK: true},
-		{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: true, laxOK: true},
+		{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: true},
+		{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: true},
+		{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: 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},
+
+		// toolchain lines
+		{desc: "tool", input: "module m\ntoolchain go1.2\n", ok: true},
+		{desc: "tool1", input: "module m\ntoolchain go1.2.3\n", ok: true},
+		{desc: "tool2", input: "module m\ntoolchain go1.2rc1\n", ok: true},
+		{desc: "tool3", input: "module m\ntoolchain gccgo-go1.2rc1\n", ok: true},
+		{desc: "tool4", input: "module m\ntoolchain local\n", ok: true},
+		{desc: "tool5", input: "module m\ntoolchain inconceivable!\n", ok: false, laxOK: true},
 	}
 	t.Run("Strict", func(t *testing.T) {
 		for _, test := range tests {
@@ -462,7 +477,7 @@
 	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) {
+				if _, err := ParseLax("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)
diff --git a/modfile/rule.go b/modfile/rule.go
index 3306f6f..45e7b6a 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -375,6 +375,21 @@
 		f.Go = &Go{Syntax: line}
 		f.Go.Version = args[0]
 
+	case "toolchain":
+		if f.Toolchain != nil {
+			errorf("repeated toolchain statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("toolchain directive expects exactly one argument")
+			return
+		} else if strict && !ToolchainRE.MatchString(args[0]) {
+			errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0])
+			return
+		}
+		f.Toolchain = &Toolchain{Syntax: line}
+		f.Toolchain.Name = args[0]
+
 	case "module":
 		if f.Module != nil {
 			errorf("repeated module statement")
@@ -623,6 +638,22 @@
 		f.Go = &Go{Syntax: line}
 		f.Go.Version = args[0]
 
+	case "toolchain":
+		if f.Toolchain != nil {
+			errorf("repeated toolchain statement")
+			return
+		}
+		if len(args) != 1 {
+			errorf("toolchain directive expects exactly one argument")
+			return
+		} else if !ToolchainRE.MatchString(args[0]) {
+			errorf("invalid toolchain version '%s': must match format go1.23 or local", args[0])
+			return
+		}
+
+		f.Toolchain = &Toolchain{Syntax: line}
+		f.Toolchain.Name = args[0]
+
 	case "use":
 		if len(args) != 1 {
 			errorf("usage: %s local/dir", verb)
diff --git a/modfile/testdata/goline.golden b/modfile/testdata/goline.golden
new file mode 100644
index 0000000..1f07989
--- /dev/null
+++ b/modfile/testdata/goline.golden
@@ -0,0 +1,3 @@
+go 1.2.3
+
+toolchain local
diff --git a/modfile/testdata/goline.in b/modfile/testdata/goline.in
new file mode 100644
index 0000000..498c1b8
--- /dev/null
+++ b/modfile/testdata/goline.in
@@ -0,0 +1,2 @@
+go 1.2.3
+toolchain local
diff --git a/modfile/testdata/work/goline.golden b/modfile/testdata/work/goline.golden
new file mode 100644
index 0000000..1f07989
--- /dev/null
+++ b/modfile/testdata/work/goline.golden
@@ -0,0 +1,3 @@
+go 1.2.3
+
+toolchain local
diff --git a/modfile/testdata/work/goline.in b/modfile/testdata/work/goline.in
new file mode 100644
index 0000000..498c1b8
--- /dev/null
+++ b/modfile/testdata/work/goline.in
@@ -0,0 +1,2 @@
+go 1.2.3
+toolchain local
diff --git a/modfile/work_test.go b/modfile/work_test.go
index 46115a5..096ed5c 100644
--- a/modfile/work_test.go
+++ b/modfile/work_test.go
@@ -332,10 +332,7 @@
 
 			pf1, err := ParseWork(base, data, nil)
 			if err != nil {
-				switch base {
-				case "testdata/replace2.in", "testdata/gopkg.in.golden":
-					t.Errorf("should parse %v: %v", base, err)
-				}
+				t.Errorf("should parse %v: %v", base, err)
 			}
 			if err == nil {
 				pf2, err := ParseWork(base, ndata, nil)