summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/gover
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
commitccd992355df7192993c666236047820244914598 (patch)
treef00fea65147227b7743083c6148396f74cd66935 /src/cmd/go/internal/gover
parentInitial commit. (diff)
downloadgolang-1.21-ccd992355df7192993c666236047820244914598.tar.xz
golang-1.21-ccd992355df7192993c666236047820244914598.zip
Adding upstream version 1.21.8.upstream/1.21.8
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/go/internal/gover')
-rw-r--r--src/cmd/go/internal/gover/gomod.go43
-rw-r--r--src/cmd/go/internal/gover/gover.go254
-rw-r--r--src/cmd/go/internal/gover/gover_test.go160
-rw-r--r--src/cmd/go/internal/gover/local.go42
-rw-r--r--src/cmd/go/internal/gover/mod.go127
-rw-r--r--src/cmd/go/internal/gover/mod_test.go72
-rw-r--r--src/cmd/go/internal/gover/toolchain.go98
-rw-r--r--src/cmd/go/internal/gover/toolchain_test.go19
-rw-r--r--src/cmd/go/internal/gover/version.go74
9 files changed, 889 insertions, 0 deletions
diff --git a/src/cmd/go/internal/gover/gomod.go b/src/cmd/go/internal/gover/gomod.go
new file mode 100644
index 0000000..4a4ae53
--- /dev/null
+++ b/src/cmd/go/internal/gover/gomod.go
@@ -0,0 +1,43 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "bytes"
+ "strings"
+)
+
+var nl = []byte("\n")
+
+// GoModLookup takes go.mod or go.work content,
+// finds the first line in the file starting with the given key,
+// and returns the value associated with that key.
+//
+// Lookup should only be used with non-factored verbs
+// such as "go" and "toolchain", usually to find versions
+// or version-like strings.
+func GoModLookup(gomod []byte, key string) string {
+ for len(gomod) > 0 {
+ var line []byte
+ line, gomod, _ = bytes.Cut(gomod, nl)
+ line = bytes.TrimSpace(line)
+ if v, ok := parseKey(line, key); ok {
+ return v
+ }
+ }
+ return ""
+}
+
+func parseKey(line []byte, key string) (string, bool) {
+ if !strings.HasPrefix(string(line), key) {
+ return "", false
+ }
+ s := strings.TrimPrefix(string(line), key)
+ if len(s) == 0 || (s[0] != ' ' && s[0] != '\t') {
+ return "", false
+ }
+ s, _, _ = strings.Cut(s, "//") // strip comments
+ return strings.TrimSpace(s), true
+}
diff --git a/src/cmd/go/internal/gover/gover.go b/src/cmd/go/internal/gover/gover.go
new file mode 100644
index 0000000..b2a8261
--- /dev/null
+++ b/src/cmd/go/internal/gover/gover.go
@@ -0,0 +1,254 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package gover implements support for Go toolchain versions like 1.21.0 and 1.21rc1.
+// (For historical reasons, Go does not use semver for its toolchains.)
+// This package provides the same basic analysis that golang.org/x/mod/semver does for semver.
+// It also provides some helpers for extracting versions from go.mod files
+// and for dealing with module.Versions that may use Go versions or semver
+// depending on the module path.
+package gover
+
+import (
+ "cmp"
+)
+
+// A version is a parsed Go version: major[.minor[.patch]][kind[pre]]
+// The numbers are the original decimal strings to avoid integer overflows
+// and since there is very little actual math. (Probably overflow doesn't matter in practice,
+// but at the time this code was written, there was an existing test that used
+// go1.99999999999, which does not fit in an int on 32-bit platforms.
+// The "big decimal" representation avoids the problem entirely.)
+type version struct {
+ major string // decimal
+ minor string // decimal or ""
+ patch string // decimal or ""
+ kind string // "", "alpha", "beta", "rc"
+ pre string // decimal or ""
+}
+
+// Compare returns -1, 0, or +1 depending on whether
+// x < y, x == y, or x > y, interpreted as toolchain versions.
+// The versions x and y must not begin with a "go" prefix: just "1.21" not "go1.21".
+// Malformed versions compare less than well-formed versions and equal to each other.
+// The language version "1.21" compares less than the release candidate and eventual releases "1.21rc1" and "1.21.0".
+func Compare(x, y string) int {
+ vx := parse(x)
+ vy := parse(y)
+
+ if c := cmpInt(vx.major, vy.major); c != 0 {
+ return c
+ }
+ if c := cmpInt(vx.minor, vy.minor); c != 0 {
+ return c
+ }
+ if c := cmpInt(vx.patch, vy.patch); c != 0 {
+ return c
+ }
+ if c := cmp.Compare(vx.kind, vy.kind); c != 0 { // "" < alpha < beta < rc
+ return c
+ }
+ if c := cmpInt(vx.pre, vy.pre); c != 0 {
+ return c
+ }
+ return 0
+}
+
+// Max returns the maximum of x and y interpreted as toolchain versions,
+// compared using Compare.
+// If x and y compare equal, Max returns x.
+func Max(x, y string) string {
+ if Compare(x, y) < 0 {
+ return y
+ }
+ return x
+}
+
+// Toolchain returns the maximum of x and y interpreted as toolchain names,
+// compared using Compare(FromToolchain(x), FromToolchain(y)).
+// If x and y compare equal, Max returns x.
+func ToolchainMax(x, y string) string {
+ if Compare(FromToolchain(x), FromToolchain(y)) < 0 {
+ return y
+ }
+ return x
+}
+
+// IsLang reports whether v denotes the overall Go language version
+// and not a specific release. Starting with the Go 1.21 release, "1.x" denotes
+// the overall language version; the first release is "1.x.0".
+// The distinction is important because the relative ordering is
+//
+// 1.21 < 1.21rc1 < 1.21.0
+//
+// meaning that Go 1.21rc1 and Go 1.21.0 will both handle go.mod files that
+// say "go 1.21", but Go 1.21rc1 will not handle files that say "go 1.21.0".
+func IsLang(x string) bool {
+ v := parse(x)
+ return v != version{} && v.patch == "" && v.kind == "" && v.pre == ""
+}
+
+// Lang returns the Go language version. For example, Lang("1.2.3") == "1.2".
+func Lang(x string) string {
+ v := parse(x)
+ if v.minor == "" {
+ return v.major
+ }
+ return v.major + "." + v.minor
+}
+
+// IsPrerelease reports whether v denotes a Go prerelease version.
+func IsPrerelease(x string) bool {
+ return parse(x).kind != ""
+}
+
+// Prev returns the Go major release immediately preceding v,
+// or v itself if v is the first Go major release (1.0) or not a supported
+// Go version.
+//
+// Examples:
+//
+// Prev("1.2") = "1.1"
+// Prev("1.3rc4") = "1.2"
+func Prev(x string) string {
+ v := parse(x)
+ if cmpInt(v.minor, "1") <= 0 {
+ return v.major
+ }
+ return v.major + "." + decInt(v.minor)
+}
+
+// IsValid reports whether the version x is valid.
+func IsValid(x string) bool {
+ return parse(x) != version{}
+}
+
+// parse parses the Go version string x into a version.
+// It returns the zero version if x is malformed.
+func parse(x string) version {
+ var v version
+
+ // Parse major version.
+ var ok bool
+ v.major, x, ok = cutInt(x)
+ if !ok {
+ return version{}
+ }
+ if x == "" {
+ // Interpret "1" as "1.0.0".
+ v.minor = "0"
+ v.patch = "0"
+ return v
+ }
+
+ // Parse . before minor version.
+ if x[0] != '.' {
+ return version{}
+ }
+
+ // Parse minor version.
+ v.minor, x, ok = cutInt(x[1:])
+ if !ok {
+ return version{}
+ }
+ if x == "" {
+ // Patch missing is same as "0" for older versions.
+ // Starting in Go 1.21, patch missing is different from explicit .0.
+ if cmpInt(v.minor, "21") < 0 {
+ v.patch = "0"
+ }
+ return v
+ }
+
+ // Parse patch if present.
+ if x[0] == '.' {
+ v.patch, x, ok = cutInt(x[1:])
+ if !ok || x != "" {
+ // Note that we are disallowing prereleases (alpha, beta, rc) for patch releases here (x != "").
+ // Allowing them would be a bit confusing because we already have:
+ // 1.21 < 1.21rc1
+ // But a prerelease of a patch would have the opposite effect:
+ // 1.21.3rc1 < 1.21.3
+ // We've never needed them before, so let's not start now.
+ return version{}
+ }
+ return v
+ }
+
+ // Parse prerelease.
+ i := 0
+ for i < len(x) && (x[i] < '0' || '9' < x[i]) {
+ if x[i] < 'a' || 'z' < x[i] {
+ return version{}
+ }
+ i++
+ }
+ if i == 0 {
+ return version{}
+ }
+ v.kind, x = x[:i], x[i:]
+ if x == "" {
+ return v
+ }
+ v.pre, x, ok = cutInt(x)
+ if !ok || x != "" {
+ return version{}
+ }
+
+ return v
+}
+
+// cutInt scans the leading decimal number at the start of x to an integer
+// and returns that value and the rest of the string.
+func cutInt(x string) (n, rest string, ok bool) {
+ i := 0
+ for i < len(x) && '0' <= x[i] && x[i] <= '9' {
+ i++
+ }
+ if i == 0 || x[0] == '0' && i != 1 {
+ return "", "", false
+ }
+ return x[:i], x[i:], true
+}
+
+// cmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers.
+// (Copied from golang.org/x/mod/semver's compareInt.)
+func cmpInt(x, y string) int {
+ if x == y {
+ return 0
+ }
+ if len(x) < len(y) {
+ return -1
+ }
+ if len(x) > len(y) {
+ return +1
+ }
+ if x < y {
+ return -1
+ } else {
+ return +1
+ }
+}
+
+// decInt returns the decimal string decremented by 1, or the empty string
+// if the decimal is all zeroes.
+// (Copied from golang.org/x/mod/module's decDecimal.)
+func decInt(decimal string) string {
+ // Scan right to left turning 0s to 9s until you find a digit to decrement.
+ digits := []byte(decimal)
+ i := len(digits) - 1
+ for ; i >= 0 && digits[i] == '0'; i-- {
+ digits[i] = '9'
+ }
+ if i < 0 {
+ // decimal is all zeros
+ return ""
+ }
+ if i == 0 && digits[i] == '1' && len(digits) > 1 {
+ digits = digits[1:]
+ } else {
+ digits[i]--
+ }
+ return string(digits)
+}
diff --git a/src/cmd/go/internal/gover/gover_test.go b/src/cmd/go/internal/gover/gover_test.go
new file mode 100644
index 0000000..3a0bf10
--- /dev/null
+++ b/src/cmd/go/internal/gover/gover_test.go
@@ -0,0 +1,160 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestCompare(t *testing.T) { test2(t, compareTests, "Compare", Compare) }
+
+var compareTests = []testCase2[string, string, int]{
+ {"", "", 0},
+ {"x", "x", 0},
+ {"", "x", 0},
+ {"1", "1.1", -1},
+ {"1.5", "1.6", -1},
+ {"1.5", "1.10", -1},
+ {"1.6", "1.6.1", -1},
+ {"1.19", "1.19.0", 0},
+ {"1.19rc1", "1.19", -1},
+ {"1.20", "1.20.0", 0},
+ {"1.20rc1", "1.20", -1},
+ {"1.21", "1.21.0", -1},
+ {"1.21", "1.21rc1", -1},
+ {"1.21rc1", "1.21.0", -1},
+ {"1.6", "1.19", -1},
+ {"1.19", "1.19.1", -1},
+ {"1.19rc1", "1.19", -1},
+ {"1.19rc1", "1.19.1", -1},
+ {"1.19rc1", "1.19rc2", -1},
+ {"1.19.0", "1.19.1", -1},
+ {"1.19rc1", "1.19.0", -1},
+ {"1.19alpha3", "1.19beta2", -1},
+ {"1.19beta2", "1.19rc1", -1},
+ {"1.1", "1.99999999999999998", -1},
+ {"1.99999999999999998", "1.99999999999999999", -1},
+}
+
+func TestParse(t *testing.T) { test1(t, parseTests, "parse", parse) }
+
+var parseTests = []testCase1[string, version]{
+ {"1", version{"1", "0", "0", "", ""}},
+ {"1.2", version{"1", "2", "0", "", ""}},
+ {"1.2.3", version{"1", "2", "3", "", ""}},
+ {"1.2rc3", version{"1", "2", "", "rc", "3"}},
+ {"1.20", version{"1", "20", "0", "", ""}},
+ {"1.21", version{"1", "21", "", "", ""}},
+ {"1.21rc3", version{"1", "21", "", "rc", "3"}},
+ {"1.21.0", version{"1", "21", "0", "", ""}},
+ {"1.24", version{"1", "24", "", "", ""}},
+ {"1.24rc3", version{"1", "24", "", "rc", "3"}},
+ {"1.24.0", version{"1", "24", "0", "", ""}},
+ {"1.999testmod", version{"1", "999", "", "testmod", ""}},
+ {"1.99999999999999999", version{"1", "99999999999999999", "", "", ""}},
+}
+
+func TestLang(t *testing.T) { test1(t, langTests, "Lang", Lang) }
+
+var langTests = []testCase1[string, string]{
+ {"1.2rc3", "1.2"},
+ {"1.2.3", "1.2"},
+ {"1.2", "1.2"},
+ {"1", "1.0"},
+ {"1.999testmod", "1.999"},
+}
+
+func TestIsLang(t *testing.T) { test1(t, isLangTests, "IsLang", IsLang) }
+
+var isLangTests = []testCase1[string, bool]{
+ {"1.2rc3", false},
+ {"1.2.3", false},
+ {"1.999testmod", false},
+ {"1.22", true},
+ {"1.21", true},
+ {"1.20", false}, // == 1.20.0
+ {"1.19", false}, // == 1.20.0
+ {"1.3", false}, // == 1.3.0
+ {"1.2", false}, // == 1.2.0
+ {"1", false}, // == 1.0.0
+}
+
+func TestPrev(t *testing.T) { test1(t, prevTests, "Prev", Prev) }
+
+var prevTests = []testCase1[string, string]{
+ {"", ""},
+ {"0", "0"},
+ {"1.3rc4", "1.2"},
+ {"1.3.5", "1.2"},
+ {"1.3", "1.2"},
+ {"1", "1"},
+ {"1.99999999999999999", "1.99999999999999998"},
+ {"1.40000000000000000", "1.39999999999999999"},
+}
+
+func TestIsValid(t *testing.T) { test1(t, isValidTests, "IsValid", IsValid) }
+
+var isValidTests = []testCase1[string, bool]{
+ {"1.2rc3", true},
+ {"1.2.3", true},
+ {"1.999testmod", true},
+ {"1.600+auto", false},
+ {"1.22", true},
+ {"1.21.0", true},
+ {"1.21rc2", true},
+ {"1.21", true},
+ {"1.20.0", true},
+ {"1.20", true},
+ {"1.19", true},
+ {"1.3", true},
+ {"1.2", true},
+ {"1", true},
+}
+
+type testCase1[In, Out any] struct {
+ in In
+ out Out
+}
+
+type testCase2[In1, In2, Out any] struct {
+ in1 In1
+ in2 In2
+ out Out
+}
+
+type testCase3[In1, In2, In3, Out any] struct {
+ in1 In1
+ in2 In2
+ in3 In3
+ out Out
+}
+
+func test1[In, Out any](t *testing.T, tests []testCase1[In, Out], name string, f func(In) Out) {
+ t.Helper()
+ for _, tt := range tests {
+ if out := f(tt.in); !reflect.DeepEqual(out, tt.out) {
+ t.Errorf("%s(%v) = %v, want %v", name, tt.in, out, tt.out)
+ }
+ }
+}
+
+func test2[In1, In2, Out any](t *testing.T, tests []testCase2[In1, In2, Out], name string, f func(In1, In2) Out) {
+ t.Helper()
+ for _, tt := range tests {
+ if out := f(tt.in1, tt.in2); !reflect.DeepEqual(out, tt.out) {
+ t.Errorf("%s(%+v, %+v) = %+v, want %+v", name, tt.in1, tt.in2, out, tt.out)
+ }
+ }
+}
+
+func test3[In1, In2, In3, Out any](t *testing.T, tests []testCase3[In1, In2, In3, Out], name string, f func(In1, In2, In3) Out) {
+ t.Helper()
+ for _, tt := range tests {
+ if out := f(tt.in1, tt.in2, tt.in3); !reflect.DeepEqual(out, tt.out) {
+ t.Errorf("%s(%+v, %+v, %+v) = %+v, want %+v", name, tt.in1, tt.in2, tt.in3, out, tt.out)
+ }
+ }
+}
diff --git a/src/cmd/go/internal/gover/local.go b/src/cmd/go/internal/gover/local.go
new file mode 100644
index 0000000..8183a5c
--- /dev/null
+++ b/src/cmd/go/internal/gover/local.go
@@ -0,0 +1,42 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "internal/goversion"
+ "runtime"
+ "strconv"
+)
+
+// TestVersion is initialized in the go command test binary
+// to be $TESTGO_VERSION, to allow tests to override the
+// go command's idea of its own version as returned by Local.
+var TestVersion string
+
+// Local returns the local Go version, the one implemented by this go command.
+func Local() string {
+ v, _ := local()
+ return v
+}
+
+// LocalToolchain returns the local toolchain name, the one implemented by this go command.
+func LocalToolchain() string {
+ _, t := local()
+ return t
+}
+
+func local() (goVers, toolVers string) {
+ toolVers = runtime.Version()
+ if TestVersion != "" {
+ toolVers = TestVersion
+ }
+ goVers = FromToolchain(toolVers)
+ if goVers == "" {
+ // Development branch. Use "Dev" version with just 1.N, no rc1 or .0 suffix.
+ goVers = "1." + strconv.Itoa(goversion.Version)
+ toolVers = "go" + goVers
+ }
+ return goVers, toolVers
+}
diff --git a/src/cmd/go/internal/gover/mod.go b/src/cmd/go/internal/gover/mod.go
new file mode 100644
index 0000000..d3cc170
--- /dev/null
+++ b/src/cmd/go/internal/gover/mod.go
@@ -0,0 +1,127 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "sort"
+ "strings"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
+)
+
+// IsToolchain reports whether the module path corresponds to the
+// virtual, non-downloadable module tracking go or toolchain directives in the go.mod file.
+//
+// Note that IsToolchain only matches "go" and "toolchain", not the
+// real, downloadable module "golang.org/toolchain" containing toolchain files.
+//
+// IsToolchain("go") = true
+// IsToolchain("toolchain") = true
+// IsToolchain("golang.org/x/tools") = false
+// IsToolchain("golang.org/toolchain") = false
+func IsToolchain(path string) bool {
+ return path == "go" || path == "toolchain"
+}
+
+// ModCompare returns the result of comparing the versions x and y
+// for the module with the given path.
+// The path is necessary because the "go" and "toolchain" modules
+// use a different version syntax and semantics (gover, this package)
+// than most modules (semver).
+func ModCompare(path string, x, y string) int {
+ if path == "go" {
+ return Compare(x, y)
+ }
+ if path == "toolchain" {
+ return Compare(maybeToolchainVersion(x), maybeToolchainVersion(y))
+ }
+ return semver.Compare(x, y)
+}
+
+// ModSort is like module.Sort but understands the "go" and "toolchain"
+// modules and their version ordering.
+func ModSort(list []module.Version) {
+ sort.Slice(list, func(i, j int) bool {
+ mi := list[i]
+ mj := list[j]
+ if mi.Path != mj.Path {
+ return mi.Path < mj.Path
+ }
+ // To help go.sum formatting, allow version/file.
+ // Compare semver prefix by semver rules,
+ // file by string order.
+ vi := mi.Version
+ vj := mj.Version
+ var fi, fj string
+ if k := strings.Index(vi, "/"); k >= 0 {
+ vi, fi = vi[:k], vi[k:]
+ }
+ if k := strings.Index(vj, "/"); k >= 0 {
+ vj, fj = vj[:k], vj[k:]
+ }
+ if vi != vj {
+ return ModCompare(mi.Path, vi, vj) < 0
+ }
+ return fi < fj
+ })
+}
+
+// ModIsValid reports whether vers is a valid version syntax for the module with the given path.
+func ModIsValid(path, vers string) bool {
+ if IsToolchain(path) {
+ if path == "toolchain" {
+ return IsValid(FromToolchain(vers))
+ }
+ return IsValid(vers)
+ }
+ return semver.IsValid(vers)
+}
+
+// ModIsPrefix reports whether v is a valid version syntax prefix for the module with the given path.
+// The caller is assumed to have checked that ModIsValid(path, vers) is true.
+func ModIsPrefix(path, vers string) bool {
+ if IsToolchain(path) {
+ if path == "toolchain" {
+ return IsLang(FromToolchain(vers))
+ }
+ return IsLang(vers)
+ }
+ // Semver
+ dots := 0
+ for i := 0; i < len(vers); i++ {
+ switch vers[i] {
+ case '-', '+':
+ return false
+ case '.':
+ dots++
+ if dots >= 2 {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// ModIsPrerelease reports whether v is a prerelease version for the module with the given path.
+// The caller is assumed to have checked that ModIsValid(path, vers) is true.
+func ModIsPrerelease(path, vers string) bool {
+ if IsToolchain(path) {
+ return IsPrerelease(vers)
+ }
+ return semver.Prerelease(vers) != ""
+}
+
+// ModMajorMinor returns the "major.minor" truncation of the version v,
+// for use as a prefix in "@patch" queries.
+func ModMajorMinor(path, vers string) string {
+ if IsToolchain(path) {
+ if path == "toolchain" {
+ return "go" + Lang(FromToolchain(vers))
+ }
+ return Lang(vers)
+ }
+ return semver.MajorMinor(vers)
+}
diff --git a/src/cmd/go/internal/gover/mod_test.go b/src/cmd/go/internal/gover/mod_test.go
new file mode 100644
index 0000000..c92169c
--- /dev/null
+++ b/src/cmd/go/internal/gover/mod_test.go
@@ -0,0 +1,72 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "slices"
+ "strings"
+ "testing"
+
+ "golang.org/x/mod/module"
+)
+
+func TestIsToolchain(t *testing.T) { test1(t, isToolchainTests, "IsToolchain", IsToolchain) }
+
+var isToolchainTests = []testCase1[string, bool]{
+ {"go", true},
+ {"toolchain", true},
+ {"anything", false},
+ {"golang.org/toolchain", false},
+}
+
+func TestModCompare(t *testing.T) { test3(t, modCompareTests, "ModCompare", ModCompare) }
+
+var modCompareTests = []testCase3[string, string, string, int]{
+ {"go", "1.2", "1.3", -1},
+ {"go", "v1.2", "v1.3", 0}, // equal because invalid
+ {"go", "1.2", "1.2", 0},
+ {"toolchain", "go1.2", "go1.3", -1},
+ {"toolchain", "go1.2", "go1.2", 0},
+ {"toolchain", "1.2", "1.3", -1}, // accepted but non-standard
+ {"toolchain", "v1.2", "v1.3", 0}, // equal because invalid
+ {"rsc.io/quote", "v1.2", "v1.3", -1},
+ {"rsc.io/quote", "1.2", "1.3", 0}, // equal because invalid
+}
+
+func TestModIsValid(t *testing.T) { test2(t, modIsValidTests, "ModIsValid", ModIsValid) }
+
+var modIsValidTests = []testCase2[string, string, bool]{
+ {"go", "1.2", true},
+ {"go", "v1.2", false},
+ {"toolchain", "go1.2", true},
+ {"toolchain", "v1.2", false},
+ {"rsc.io/quote", "v1.2", true},
+ {"rsc.io/quote", "1.2", false},
+}
+
+func TestModSort(t *testing.T) {
+ test1(t, modSortTests, "ModSort", func(list []module.Version) []module.Version {
+ out := slices.Clone(list)
+ ModSort(out)
+ return out
+ })
+}
+
+var modSortTests = []testCase1[[]module.Version, []module.Version]{
+ {
+ mvl(`z v1.1; a v1.2; a v1.1; go 1.3; toolchain 1.3; toolchain 1.2; go 1.2`),
+ mvl(`a v1.1; a v1.2; go 1.2; go 1.3; toolchain 1.2; toolchain 1.3; z v1.1`),
+ },
+}
+
+func mvl(s string) []module.Version {
+ var list []module.Version
+ for _, f := range strings.Split(s, ";") {
+ f = strings.TrimSpace(f)
+ path, vers, _ := strings.Cut(f, " ")
+ list = append(list, module.Version{Path: path, Version: vers})
+ }
+ return list
+}
diff --git a/src/cmd/go/internal/gover/toolchain.go b/src/cmd/go/internal/gover/toolchain.go
new file mode 100644
index 0000000..a24df98
--- /dev/null
+++ b/src/cmd/go/internal/gover/toolchain.go
@@ -0,0 +1,98 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import (
+ "cmd/go/internal/base"
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+)
+
+// FromToolchain returns the Go version for the named toolchain,
+// derived from the name itself (not by running the toolchain).
+// A toolchain is named "goVERSION".
+// A suffix after the VERSION introduced by a -, space, or tab is removed.
+// Examples:
+//
+// FromToolchain("go1.2.3") == "1.2.3"
+// FromToolchain("go1.2.3-bigcorp") == "1.2.3"
+// FromToolchain("invalid") == ""
+func FromToolchain(name string) string {
+ if strings.ContainsAny(name, "\\/") {
+ // The suffix must not include a path separator, since that would cause
+ // exec.LookPath to resolve it from a relative directory instead of from
+ // $PATH.
+ return ""
+ }
+
+ var v string
+ if strings.HasPrefix(name, "go") {
+ v = name[2:]
+ } else {
+ return ""
+ }
+ // Some builds use custom suffixes; strip them.
+ if i := strings.IndexAny(v, " \t-"); i >= 0 {
+ v = v[:i]
+ }
+ if !IsValid(v) {
+ return ""
+ }
+ return v
+}
+
+func maybeToolchainVersion(name string) string {
+ if IsValid(name) {
+ return name
+ }
+ return FromToolchain(name)
+}
+
+// Startup records the information that went into the startup-time version switch.
+// It is initialized by switchGoToolchain.
+var Startup struct {
+ GOTOOLCHAIN string // $GOTOOLCHAIN setting
+ AutoFile string // go.mod or go.work file consulted
+ AutoGoVersion string // go line found in file
+ AutoToolchain string // toolchain line found in file
+}
+
+// A TooNewError explains that a module is too new for this version of Go.
+type TooNewError struct {
+ What string
+ GoVersion string
+ Toolchain string // for callers if they want to use it, but not printed
+}
+
+func (e *TooNewError) Error() string {
+ var explain string
+ if Startup.GOTOOLCHAIN != "" && Startup.GOTOOLCHAIN != "auto" {
+ explain = "; GOTOOLCHAIN=" + Startup.GOTOOLCHAIN
+ }
+ if Startup.AutoFile != "" && (Startup.AutoGoVersion != "" || Startup.AutoToolchain != "") {
+ explain += fmt.Sprintf("; %s sets ", base.ShortPath(Startup.AutoFile))
+ if Startup.AutoToolchain != "" {
+ explain += "toolchain " + Startup.AutoToolchain
+ } else {
+ explain += "go " + Startup.AutoGoVersion
+ }
+ }
+ return fmt.Sprintf("%v requires go >= %v (running go %v%v)", e.What, e.GoVersion, Local(), explain)
+}
+
+var ErrTooNew = errors.New("module too new")
+
+func (e *TooNewError) Is(err error) bool {
+ return err == ErrTooNew
+}
+
+// A Switcher provides the ability to switch to a new toolchain in response to TooNewErrors.
+// See [cmd/go/internal/toolchain.Switcher] for documentation.
+type Switcher interface {
+ Error(err error)
+ Switch(ctx context.Context)
+}
diff --git a/src/cmd/go/internal/gover/toolchain_test.go b/src/cmd/go/internal/gover/toolchain_test.go
new file mode 100644
index 0000000..d1c22fb
--- /dev/null
+++ b/src/cmd/go/internal/gover/toolchain_test.go
@@ -0,0 +1,19 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import "testing"
+
+func TestFromToolchain(t *testing.T) { test1(t, fromToolchainTests, "FromToolchain", FromToolchain) }
+
+var fromToolchainTests = []testCase1[string, string]{
+ {"go1.2.3", "1.2.3"},
+ {"1.2.3", ""},
+ {"go1.2.3+bigcorp", ""},
+ {"go1.2.3-bigcorp", "1.2.3"},
+ {"go1.2.3-bigcorp more text", "1.2.3"},
+ {"gccgo-go1.23rc4", ""},
+ {"gccgo-go1.23rc4-bigdwarf", ""},
+}
diff --git a/src/cmd/go/internal/gover/version.go b/src/cmd/go/internal/gover/version.go
new file mode 100644
index 0000000..2681013
--- /dev/null
+++ b/src/cmd/go/internal/gover/version.go
@@ -0,0 +1,74 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gover
+
+import "golang.org/x/mod/modfile"
+
+const (
+ // narrowAllVersion is the Go version at which the
+ // module-module "all" pattern no longer closes over the dependencies of
+ // tests outside of the main module.
+ NarrowAllVersion = "1.16"
+
+ // DefaultGoModVersion is the Go version to assume for go.mod files
+ // that do not declare a Go version. The go command has been
+ // writing go versions to modules since Go 1.12, so a go.mod
+ // without a version is either very old or recently hand-written.
+ // Since we can't tell which, we have to assume it's very old.
+ // The semantics of the go.mod changed at Go 1.17 to support
+ // graph pruning. If see a go.mod without a go line, we have to
+ // assume Go 1.16 so that we interpret the requirements correctly.
+ // Note that this default must stay at Go 1.16; it cannot be moved forward.
+ DefaultGoModVersion = "1.16"
+
+ // DefaultGoWorkVersion is the Go version to assume for go.work files
+ // that do not declare a Go version. Workspaces were added in Go 1.18,
+ // so use that.
+ DefaultGoWorkVersion = "1.18"
+
+ // ExplicitIndirectVersion is the Go version at which a
+ // module's go.mod file is expected to list explicit requirements on every
+ // module that provides any package transitively imported by that module.
+ //
+ // Other indirect dependencies of such a module can be safely pruned out of
+ // the module graph; see https://golang.org/ref/mod#graph-pruning.
+ ExplicitIndirectVersion = "1.17"
+
+ // separateIndirectVersion is the Go version at which
+ // "// indirect" dependencies are added in a block separate from the direct
+ // ones. See https://golang.org/issue/45965.
+ SeparateIndirectVersion = "1.17"
+
+ // tidyGoModSumVersion is the Go version at which
+ // 'go mod tidy' preserves go.mod checksums needed to build test dependencies
+ // of packages in "all", so that 'go test all' can be run without checksum
+ // errors.
+ // See https://go.dev/issue/56222.
+ TidyGoModSumVersion = "1.21"
+
+ // goStrictVersion is the Go version at which the Go versions
+ // became "strict" in the sense that, restricted to modules at this version
+ // or later, every module must have a go version line ≥ all its dependencies.
+ // It is also the version after which "too new" a version is considered a fatal error.
+ GoStrictVersion = "1.21"
+)
+
+// FromGoMod returns the go version from the go.mod file.
+// It returns DefaultGoModVersion if the go.mod file does not contain a go line or if mf is nil.
+func FromGoMod(mf *modfile.File) string {
+ if mf == nil || mf.Go == nil {
+ return DefaultGoModVersion
+ }
+ return mf.Go.Version
+}
+
+// FromGoWork returns the go version from the go.mod file.
+// It returns DefaultGoWorkVersion if the go.mod file does not contain a go line or if wf is nil.
+func FromGoWork(wf *modfile.WorkFile) string {
+ if wf == nil || wf.Go == nil {
+ return DefaultGoWorkVersion
+ }
+ return wf.Go.Version
+}