// 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 } // 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 } // 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]) { 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) }