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".
11package gover
12
13import (
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.)
23type 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".
36func 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.
61func 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".
77func 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".
83func 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.
92func 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.
98func 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.
173func 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.)
186func 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.)
206func 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