diff options
Diffstat (limited to 'src/internal/gover/gover.go')
-rw-r--r-- | src/internal/gover/gover.go | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/src/internal/gover/gover.go b/src/internal/gover/gover.go new file mode 100644 index 0000000..2ad0684 --- /dev/null +++ b/src/internal/gover/gover.go @@ -0,0 +1,223 @@ +// 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. +// +// The go/version package should be imported instead of this one when possible. +// Note that this package works on "1.21" while go/version works on "go1.21". +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 +} + +// 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 == "" || v.Major == "1" && v.Minor == "0" { + return v.Major + } + return v.Major + "." + 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 { // no digits or unnecessary leading zero + 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) +} |