Skip to content

Commit b54cae2

Browse files
rscgopherbot
authored andcommitted
go/version: add new package
go/version provides basic comparison of Go versions, for use when deciding whether certain language features are allowed, and so on. See the proposal issue #62039 for more details. Fixes #62039 Change-Id: Ibdfd4fe15afe406c46da568cb31feb42ec30b530 Reviewed-on: https://go-review.googlesource.com/c/go/+/538895 Auto-Submit: Russ Cox <rsc@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Bryan Mills <bcmills@google.com>
1 parent 6458c8e commit b54cae2

File tree

9 files changed

+545
-209
lines changed

9 files changed

+545
-209
lines changed

api/next/62039.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pkg go/version, func Compare(string, string) int #62039
2+
pkg go/version, func IsValid(string) bool #62039
3+
pkg go/version, func Lang(string) string #62039

src/cmd/go/internal/gover/gover.go

+11-190
Original file line numberDiff line numberDiff line change
@@ -11,68 +11,23 @@
1111
package gover
1212

1313
import (
14-
"cmp"
14+
"internal/gover"
1515
)
1616

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-
3117
// Compare returns -1, 0, or +1 depending on whether
3218
// x < y, x == y, or x > y, interpreted as toolchain versions.
3319
// The versions x and y must not begin with a "go" prefix: just "1.21" not "go1.21".
3420
// Malformed versions compare less than well-formed versions and equal to each other.
3521
// The language version "1.21" compares less than the release candidate and eventual releases "1.21rc1" and "1.21.0".
3622
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
23+
return gover.Compare(x, y)
5624
}
5725

5826
// Max returns the maximum of x and y interpreted as toolchain versions,
5927
// compared using Compare.
6028
// If x and y compare equal, Max returns x.
6129
func Max(x, y string) string {
62-
if Compare(x, y) < 0 {
63-
return y
64-
}
65-
return x
66-
}
67-
68-
// Toolchain returns the maximum of x and y interpreted as toolchain names,
69-
// compared using Compare(FromToolchain(x), FromToolchain(y)).
70-
// If x and y compare equal, Max returns x.
71-
func ToolchainMax(x, y string) string {
72-
if Compare(FromToolchain(x), FromToolchain(y)) < 0 {
73-
return y
74-
}
75-
return x
30+
return gover.Max(x, y)
7631
}
7732

7833
// IsLang reports whether v denotes the overall Go language version
@@ -85,22 +40,17 @@ func ToolchainMax(x, y string) string {
8540
// meaning that Go 1.21rc1 and Go 1.21.0 will both handle go.mod files that
8641
// say "go 1.21", but Go 1.21rc1 will not handle files that say "go 1.21.0".
8742
func IsLang(x string) bool {
88-
v := parse(x)
89-
return v != version{} && v.patch == "" && v.kind == "" && v.pre == ""
43+
return gover.IsLang(x)
9044
}
9145

9246
// Lang returns the Go language version. For example, Lang("1.2.3") == "1.2".
9347
func Lang(x string) string {
94-
v := parse(x)
95-
if v.minor == "" {
96-
return v.major
97-
}
98-
return v.major + "." + v.minor
48+
return gover.Lang(x)
9949
}
10050

10151
// IsPrerelease reports whether v denotes a Go prerelease version.
10252
func IsPrerelease(x string) bool {
103-
return parse(x).kind != ""
53+
return gover.Parse(x).Kind != ""
10454
}
10555

10656
// Prev returns the Go major release immediately preceding v,
@@ -112,143 +62,14 @@ func IsPrerelease(x string) bool {
11262
// Prev("1.2") = "1.1"
11363
// Prev("1.3rc4") = "1.2"
11464
func Prev(x string) string {
115-
v := parse(x)
116-
if cmpInt(v.minor, "1") <= 0 {
117-
return v.major
65+
v := gover.Parse(x)
66+
if gover.CmpInt(v.Minor, "1") <= 0 {
67+
return v.Major
11868
}
119-
return v.major + "." + decInt(v.minor)
69+
return v.Major + "." + gover.DecInt(v.Minor)
12070
}
12171

12272
// IsValid reports whether the version x is valid.
12373
func IsValid(x string) bool {
124-
return parse(x) != version{}
125-
}
126-
127-
// parse parses the Go version string x into a version.
128-
// It returns the zero version if x is malformed.
129-
func parse(x string) version {
130-
var v version
131-
132-
// Parse major version.
133-
var ok bool
134-
v.major, x, ok = cutInt(x)
135-
if !ok {
136-
return version{}
137-
}
138-
if x == "" {
139-
// Interpret "1" as "1.0.0".
140-
v.minor = "0"
141-
v.patch = "0"
142-
return v
143-
}
144-
145-
// Parse . before minor version.
146-
if x[0] != '.' {
147-
return version{}
148-
}
149-
150-
// Parse minor version.
151-
v.minor, x, ok = cutInt(x[1:])
152-
if !ok {
153-
return version{}
154-
}
155-
if x == "" {
156-
// Patch missing is same as "0" for older versions.
157-
// Starting in Go 1.21, patch missing is different from explicit .0.
158-
if cmpInt(v.minor, "21") < 0 {
159-
v.patch = "0"
160-
}
161-
return v
162-
}
163-
164-
// Parse patch if present.
165-
if x[0] == '.' {
166-
v.patch, x, ok = cutInt(x[1:])
167-
if !ok || x != "" {
168-
// Note that we are disallowing prereleases (alpha, beta, rc) for patch releases here (x != "").
169-
// Allowing them would be a bit confusing because we already have:
170-
// 1.21 < 1.21rc1
171-
// But a prerelease of a patch would have the opposite effect:
172-
// 1.21.3rc1 < 1.21.3
173-
// We've never needed them before, so let's not start now.
174-
return version{}
175-
}
176-
return v
177-
}
178-
179-
// Parse prerelease.
180-
i := 0
181-
for i < len(x) && (x[i] < '0' || '9' < x[i]) {
182-
if x[i] < 'a' || 'z' < x[i] {
183-
return version{}
184-
}
185-
i++
186-
}
187-
if i == 0 {
188-
return version{}
189-
}
190-
v.kind, x = x[:i], x[i:]
191-
if x == "" {
192-
return v
193-
}
194-
v.pre, x, ok = cutInt(x)
195-
if !ok || x != "" {
196-
return version{}
197-
}
198-
199-
return v
200-
}
201-
202-
// cutInt scans the leading decimal number at the start of x to an integer
203-
// and returns that value and the rest of the string.
204-
func cutInt(x string) (n, rest string, ok bool) {
205-
i := 0
206-
for i < len(x) && '0' <= x[i] && x[i] <= '9' {
207-
i++
208-
}
209-
if i == 0 || x[0] == '0' && i != 1 {
210-
return "", "", false
211-
}
212-
return x[:i], x[i:], true
213-
}
214-
215-
// cmpInt returns cmp.Compare(x, y) interpreting x and y as decimal numbers.
216-
// (Copied from golang.org/x/mod/semver's compareInt.)
217-
func cmpInt(x, y string) int {
218-
if x == y {
219-
return 0
220-
}
221-
if len(x) < len(y) {
222-
return -1
223-
}
224-
if len(x) > len(y) {
225-
return +1
226-
}
227-
if x < y {
228-
return -1
229-
} else {
230-
return +1
231-
}
232-
}
233-
234-
// decInt returns the decimal string decremented by 1, or the empty string
235-
// if the decimal is all zeroes.
236-
// (Copied from golang.org/x/mod/module's decDecimal.)
237-
func decInt(decimal string) string {
238-
// Scan right to left turning 0s to 9s until you find a digit to decrement.
239-
digits := []byte(decimal)
240-
i := len(digits) - 1
241-
for ; i >= 0 && digits[i] == '0'; i-- {
242-
digits[i] = '9'
243-
}
244-
if i < 0 {
245-
// decimal is all zeros
246-
return ""
247-
}
248-
if i == 0 && digits[i] == '1' && len(digits) > 1 {
249-
digits = digits[1:]
250-
} else {
251-
digits[i]--
252-
}
253-
return string(digits)
74+
return gover.IsValid(x)
25475
}

src/cmd/go/internal/gover/gover_test.go

+1-19
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,13 @@ var compareTests = []testCase2[string, string, int]{
3939
{"1.99999999999999998", "1.99999999999999999", -1},
4040
}
4141

42-
func TestParse(t *testing.T) { test1(t, parseTests, "parse", parse) }
43-
44-
var parseTests = []testCase1[string, version]{
45-
{"1", version{"1", "0", "0", "", ""}},
46-
{"1.2", version{"1", "2", "0", "", ""}},
47-
{"1.2.3", version{"1", "2", "3", "", ""}},
48-
{"1.2rc3", version{"1", "2", "", "rc", "3"}},
49-
{"1.20", version{"1", "20", "0", "", ""}},
50-
{"1.21", version{"1", "21", "", "", ""}},
51-
{"1.21rc3", version{"1", "21", "", "rc", "3"}},
52-
{"1.21.0", version{"1", "21", "0", "", ""}},
53-
{"1.24", version{"1", "24", "", "", ""}},
54-
{"1.24rc3", version{"1", "24", "", "rc", "3"}},
55-
{"1.24.0", version{"1", "24", "0", "", ""}},
56-
{"1.999testmod", version{"1", "999", "", "testmod", ""}},
57-
{"1.99999999999999999", version{"1", "99999999999999999", "", "", ""}},
58-
}
59-
6042
func TestLang(t *testing.T) { test1(t, langTests, "Lang", Lang) }
6143

6244
var langTests = []testCase1[string, string]{
6345
{"1.2rc3", "1.2"},
6446
{"1.2.3", "1.2"},
6547
{"1.2", "1.2"},
66-
{"1", "1.0"},
48+
{"1", "1"},
6749
{"1.999testmod", "1.999"},
6850
}
6951

src/cmd/go/internal/gover/toolchain.go

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ func maybeToolchainVersion(name string) string {
5252
return FromToolchain(name)
5353
}
5454

55+
// ToolchainMax returns the maximum of x and y interpreted as toolchain names,
56+
// compared using Compare(FromToolchain(x), FromToolchain(y)).
57+
// If x and y compare equal, Max returns x.
58+
func ToolchainMax(x, y string) string {
59+
if Compare(FromToolchain(x), FromToolchain(y)) < 0 {
60+
return y
61+
}
62+
return x
63+
}
64+
5565
// Startup records the information that went into the startup-time version switch.
5666
// It is initialized by switchGoToolchain.
5767
var Startup struct {

src/go/build/deps_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ var depsRules = `
270270
271271
# go parser and friends.
272272
FMT
273+
< internal/gover
274+
< go/version
273275
< go/token
274276
< go/scanner
275277
< go/ast

src/go/version/version.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 version provides operations on [Go versions].
6+
//
7+
// [Go versions]: https://go.dev/doc/toolchain#version
8+
package version // import "go/version"
9+
10+
import "internal/gover"
11+
12+
// stripGo converts from a "go1.21" version to a "1.21" version.
13+
// If v does not start with "go", stripGo returns the empty string (a known invalid version).
14+
func stripGo(v string) string {
15+
if len(v) < 2 || v[:2] != "go" {
16+
return ""
17+
}
18+
return v[2:]
19+
}
20+
21+
// Lang returns the Go language version for version x.
22+
// If x is not a valid version, Lang returns the empty string.
23+
// For example:
24+
//
25+
// Lang("go1.21rc2") = "go1.21"
26+
// Lang("go1.21.2") = "go1.21"
27+
// Lang("go1.21") = "go1.21"
28+
// Lang("go1") = "go1"
29+
// Lang("bad") = ""
30+
// Lang("1.21") = ""
31+
func Lang(x string) string {
32+
v := gover.Lang(stripGo(x))
33+
if v == "" {
34+
return ""
35+
}
36+
return x[:2+len(v)] // "go"+v without allocation
37+
}
38+
39+
// Compare returns -1, 0, or +1 depending on whether
40+
// x < y, x == y, or x > y, interpreted as Go versions.
41+
// The versions x and y must begin with a "go" prefix: "go1.21" not "1.21".
42+
// Invalid versions, including the empty string, compare less than
43+
// valid versions and equal to each other.
44+
// The language version "go1.21" compares less than the
45+
// release candidate and eventual releases "go1.21rc1" and "go1.21.0".
46+
// Custom toolchain suffixes are ignored during comparison:
47+
// "go1.21.0" and "go1.21.0-bigcorp" are equal.
48+
func Compare(x, y string) int {
49+
return gover.Compare(stripGo(x), stripGo(y))
50+
}
51+
52+
// IsValid reports whether the version x is valid.
53+
func IsValid(x string) bool {
54+
return gover.IsValid(stripGo(x))
55+
}

0 commit comments

Comments
 (0)