1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package gover implements support for Go toolchain versions like 1.21.0 and 1.21rc1. 6 // (For historical reasons, Go does not use semver for its toolchains.) 7 // This package provides the same basic analysis that golang.org/x/mod/semver does for semver. 8 // 9 // The go/version package should be imported instead of this one when possible. 10 // Note that this package works on "1.21" while go/version works on "go1.21". 11 package gover 12 13 import ( 14 "cmp" 15 ) 16 17 // A Version is a parsed Go version: major[.Minor[.Patch]][kind[pre]] 18 // The numbers are the original decimal strings to avoid integer overflows 19 // and since there is very little actual math. (Probably overflow doesn't matter in practice, 20 // but at the time this code was written, there was an existing test that used 21 // go1.99999999999, which does not fit in an int on 32-bit platforms. 22 // The "big decimal" representation avoids the problem entirely.) 23 type Version struct { 24 Major string // decimal 25 Minor string // decimal or "" 26 Patch string // decimal or "" 27 Kind string // "", "alpha", "beta", "rc" 28 Pre string // decimal or "" 29 } 30 31 // Compare returns -1, 0, or +1 depending on whether 32 // x < y, x == y, or x > y, interpreted as toolchain versions. 33 // The versions x and y must not begin with a "go" prefix: just "1.21" not "go1.21". 34 // Malformed versions compare less than well-formed versions and equal to each other. 35 // The language version "1.21" compares less than the release candidate and eventual releases "1.21rc1" and "1.21.0". 36 func Compare(x, y string) int { 37 vx := Parse(x) 38 vy := Parse(y) 39 40 if c := CmpInt(vx.Major, vy.Major); c != 0 { 41 return c 42 } 43 if c := CmpInt(vx.Minor, vy.Minor); c != 0 { 44 return c 45 } 46 if c := CmpInt(vx.Patch, vy.Patch); c != 0 { 47 return c 48 } 49 if c := cmp.Compare(vx.Kind, vy.Kind); c != 0 { // "" < alpha < beta < rc 50 return c 51 } 52 if c := CmpInt(vx.Pre, vy.Pre); c != 0 { 53 return c 54 } 55 return 0 56 } 57 58 // Max returns the maximum of x and y interpreted as toolchain versions, 59 // compared using Compare. 60 // If x and y compare equal, Max returns x. 61 func Max(x, y string) string { 62 if Compare(x, y) < 0 { 63 return y 64 } 65 return x 66 } 67 68 // IsLang reports whether v denotes the overall Go language version 69 // and not a specific release. Starting with the Go 1.21 release, "1.x" denotes 70 // the overall language version; the first release is "1.x.0". 71 // The distinction is important because the relative ordering is 72 // 73 // 1.21 < 1.21rc1 < 1.21.0 74 // 75 // meaning that Go 1.21rc1 and Go 1.21.0 will both handle go.mod files that 76 // say "go 1.21", but Go 1.21rc1 will not handle files that say "go 1.21.0". 77 func IsLang(x string) bool { 78 v := Parse(x) 79 return v != Version{} && v.Patch == "" && v.Kind == "" && v.Pre == "" 80 } 81 82 // Lang returns the Go language version. For example, Lang("1.2.3") == "1.2". 83 func Lang(x string) string { 84 v := Parse(x) 85 if v.Minor == "" || v.Major == "1" && v.Minor == "0" { 86 return v.Major 87 } 88 return v.Major + "." + v.Minor 89 } 90 91 // IsValid reports whether the version x is valid. 92 func IsValid(x string) bool { 93 return Parse(x) != Version{} 94 } 95 96 // Parse parses the Go version string x into a version. 97 // It returns the zero version if x is malformed. 98 func Parse(x string) Version { 99 var v Version 100 101 // Parse major version. 102 var ok bool 103 v.Major, x, ok = cutInt(x) 104 if !ok { 105 return Version{} 106 } 107 if x == "" { 108 // Interpret "1" as "1.0.0". 109 v.Minor = "0" 110 v.Patch = "0" 111 return v 112 } 113 114 // Parse . before minor version. 115 if x[0] != '.' { 116 return Version{} 117 } 118 119 // Parse minor version. 120 v.Minor, x, ok = cutInt(x[1:]) 121 if !ok { 122 return Version{} 123 } 124 if x == "" { 125 // Patch missing is same as "0" for older versions. 126 // Starting in Go 1.21, patch missing is different from explicit .0. 127 if CmpInt(v.Minor, "21") < 0 { 128 v.Patch = "0" 129 } 130 return v 131 } 132 133 // Parse patch if present. 134 if x[0] == '.' { 135 v.Patch, x, ok = cutInt(x[1:]) 136 if !ok || x != "" { 137 // Note that we are disallowing prereleases (alpha, beta, rc) for patch releases here (x != ""). 138 // Allowing them would be a bit confusing because we already have: 139 // 1.21 < 1.21rc1 140 // But a prerelease of a patch would have the opposite effect: 141 // 1.21.3rc1 < 1.21.3 142 // We've never needed them before, so let's not start now. 143 return Version{} 144 } 145 return v 146 } 147 148 // Parse prerelease. 149 i := 0 150 for i < len(x) && (x[i] < '0' || '9' < x[i]) { 151 if x[i] < 'a' || 'z' < x[i] { 152 return Version{} 153 } 154 i++ 155 } 156 if i == 0 { 157 return Version{} 158 } 159 v.Kind, x = x[:i], x[i:] 160 if x == "" { 161 return v 162 } 163 v.Pre, x, ok = cutInt(x) 164 if !ok || x != "" { 165 return Version{} 166 } 167 168 return v 169 } 170 171 // cutInt scans the leading decimal number at the start of x to an integer 172 // and returns that value and the rest of the string. 173 func cutInt(x string) (n, rest string, ok bool) { 174 i := 0 175 for i < len(x) && '0' <= x[i] && x[i] <= '9' { 176 i++ 177 } 178 if i == 0 || x[0] == '0' && i != 1 { // no digits or unnecessary leading zero 179 return "", "", false 180 } 181 return x[:i], x[i:], true 182 } 183 184 // CmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers. 185 // (Copied from golang.org/x/mod/semver's compareInt.) 186 func CmpInt(x, y string) int { 187 if x == y { 188 return 0 189 } 190 if len(x) < len(y) { 191 return -1 192 } 193 if len(x) > len(y) { 194 return +1 195 } 196 if x < y { 197 return -1 198 } else { 199 return +1 200 } 201 } 202 203 // DecInt returns the decimal string decremented by 1, or the empty string 204 // if the decimal is all zeroes. 205 // (Copied from golang.org/x/mod/module's decDecimal.) 206 func DecInt(decimal string) string { 207 // Scan right to left turning 0s to 9s until you find a digit to decrement. 208 digits := []byte(decimal) 209 i := len(digits) - 1 210 for ; i >= 0 && digits[i] == '0'; i-- { 211 digits[i] = '9' 212 } 213 if i < 0 { 214 // decimal is all zeros 215 return "" 216 } 217 if i == 0 && digits[i] == '1' && len(digits) > 1 { 218 digits = digits[1:] 219 } else { 220 digits[i]-- 221 } 222 return string(digits) 223 } 224