// 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 ( ) // 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 (, string) int { := Parse() := Parse() if := CmpInt(.Major, .Major); != 0 { return } if := CmpInt(.Minor, .Minor); != 0 { return } if := CmpInt(.Patch, .Patch); != 0 { return } if := cmp.Compare(.Kind, .Kind); != 0 { // "" < alpha < beta < rc return } if := CmpInt(.Pre, .Pre); != 0 { return } 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 (, string) string { if Compare(, ) < 0 { return } return } // 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 ( string) bool { := Parse() return != Version{} && .Patch == "" && .Kind == "" && .Pre == "" } // Lang returns the Go language version. For example, Lang("1.2.3") == "1.2". func ( string) string { := Parse() if .Minor == "" || .Major == "1" && .Minor == "0" { return .Major } return .Major + "." + .Minor } // IsValid reports whether the version x is valid. func ( string) bool { return Parse() != Version{} } // Parse parses the Go version string x into a version. // It returns the zero version if x is malformed. func ( string) Version { var Version // Parse major version. var bool .Major, , = cutInt() if ! { return Version{} } if == "" { // Interpret "1" as "1.0.0". .Minor = "0" .Patch = "0" return } // Parse . before minor version. if [0] != '.' { return Version{} } // Parse minor version. .Minor, , = cutInt([1:]) if ! { return Version{} } if == "" { // Patch missing is same as "0" for older versions. // Starting in Go 1.21, patch missing is different from explicit .0. if CmpInt(.Minor, "21") < 0 { .Patch = "0" } return } // Parse patch if present. if [0] == '.' { .Patch, , = cutInt([1:]) if ! || != "" { // 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 } // Parse prerelease. := 0 for < len() && ([] < '0' || '9' < []) { if [] < 'a' || 'z' < [] { return Version{} } ++ } if == 0 { return Version{} } .Kind, = [:], [:] if == "" { return } .Pre, , = cutInt() if ! || != "" { return Version{} } return } // 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( string) (, string, bool) { := 0 for < len() && '0' <= [] && [] <= '9' { ++ } if == 0 || [0] == '0' && != 1 { // no digits or unnecessary leading zero return "", "", false } return [:], [:], true } // CmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers. // (Copied from golang.org/x/mod/semver's compareInt.) func (, string) int { if == { return 0 } if len() < len() { return -1 } if len() > len() { return +1 } if < { 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 ( string) string { // Scan right to left turning 0s to 9s until you find a digit to decrement. := []byte() := len() - 1 for ; >= 0 && [] == '0'; -- { [] = '9' } if < 0 { // decimal is all zeros return "" } if == 0 && [] == '1' && len() > 1 { = [1:] } else { []-- } return string() }