Skip to content

Commit

Permalink
go/version: add new package
Browse files Browse the repository at this point in the history
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 <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Bryan Mills <[email protected]>
  • Loading branch information
rsc authored and gopherbot committed Nov 6, 2023
1 parent 6458c8e commit b54cae2
Show file tree
Hide file tree
Showing 9 changed files with 545 additions and 209 deletions.
3 changes: 3 additions & 0 deletions api/next/62039.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pkg go/version, func Compare(string, string) int #62039
pkg go/version, func IsValid(string) bool #62039
pkg go/version, func Lang(string) string #62039
201 changes: 11 additions & 190 deletions src/cmd/go/internal/gover/gover.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,68 +11,23 @@
package gover

import (
"cmp"
"internal/gover"
)

// 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
return gover.Compare(x, y)
}

// 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 Max(x, y string) string {
if Compare(x, y) < 0 {
return y
}
return x
}

// Toolchain returns the maximum of x and y interpreted as toolchain names,
// compared using Compare(FromToolchain(x), FromToolchain(y)).
// If x and y compare equal, Max returns x.
func ToolchainMax(x, y string) string {
if Compare(FromToolchain(x), FromToolchain(y)) < 0 {
return y
}
return x
return gover.Max(x, y)
}

// IsLang reports whether v denotes the overall Go language version
Expand All @@ -85,22 +40,17 @@ func ToolchainMax(x, y string) string {
// 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 == ""
return gover.IsLang(x)
}

// 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
return gover.Lang(x)
}

// IsPrerelease reports whether v denotes a Go prerelease version.
func IsPrerelease(x string) bool {
return parse(x).kind != ""
return gover.Parse(x).Kind != ""
}

// Prev returns the Go major release immediately preceding v,
Expand All @@ -112,143 +62,14 @@ func IsPrerelease(x string) bool {
// 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
v := gover.Parse(x)
if gover.CmpInt(v.Minor, "1") <= 0 {
return v.Major
}
return v.major + "." + decInt(v.minor)
return v.Major + "." + gover.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]) {
if x[i] < 'a' || 'z' < x[i] {
return version{}
}
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)
return gover.IsValid(x)
}
20 changes: 1 addition & 19 deletions src/cmd/go/internal/gover/gover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,13 @@ var compareTests = []testCase2[string, string, int]{
{"1.99999999999999998", "1.99999999999999999", -1},
}

func TestParse(t *testing.T) { test1(t, parseTests, "parse", parse) }

var parseTests = []testCase1[string, version]{
{"1", version{"1", "0", "0", "", ""}},
{"1.2", version{"1", "2", "0", "", ""}},
{"1.2.3", version{"1", "2", "3", "", ""}},
{"1.2rc3", version{"1", "2", "", "rc", "3"}},
{"1.20", version{"1", "20", "0", "", ""}},
{"1.21", version{"1", "21", "", "", ""}},
{"1.21rc3", version{"1", "21", "", "rc", "3"}},
{"1.21.0", version{"1", "21", "0", "", ""}},
{"1.24", version{"1", "24", "", "", ""}},
{"1.24rc3", version{"1", "24", "", "rc", "3"}},
{"1.24.0", version{"1", "24", "0", "", ""}},
{"1.999testmod", version{"1", "999", "", "testmod", ""}},
{"1.99999999999999999", version{"1", "99999999999999999", "", "", ""}},
}

func TestLang(t *testing.T) { test1(t, langTests, "Lang", Lang) }

var langTests = []testCase1[string, string]{
{"1.2rc3", "1.2"},
{"1.2.3", "1.2"},
{"1.2", "1.2"},
{"1", "1.0"},
{"1", "1"},
{"1.999testmod", "1.999"},
}

Expand Down
10 changes: 10 additions & 0 deletions src/cmd/go/internal/gover/toolchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ func maybeToolchainVersion(name string) string {
return FromToolchain(name)
}

// ToolchainMax returns the maximum of x and y interpreted as toolchain names,
// compared using Compare(FromToolchain(x), FromToolchain(y)).
// If x and y compare equal, Max returns x.
func ToolchainMax(x, y string) string {
if Compare(FromToolchain(x), FromToolchain(y)) < 0 {
return y
}
return x
}

// Startup records the information that went into the startup-time version switch.
// It is initialized by switchGoToolchain.
var Startup struct {
Expand Down
2 changes: 2 additions & 0 deletions src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ var depsRules = `
# go parser and friends.
FMT
< internal/gover
< go/version
< go/token
< go/scanner
< go/ast
Expand Down
55 changes: 55 additions & 0 deletions src/go/version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 version provides operations on [Go versions].
//
// [Go versions]: https://go.dev/doc/toolchain#version
package version // import "go/version"

import "internal/gover"

// stripGo converts from a "go1.21" version to a "1.21" version.
// If v does not start with "go", stripGo returns the empty string (a known invalid version).
func stripGo(v string) string {
if len(v) < 2 || v[:2] != "go" {
return ""
}
return v[2:]
}

// Lang returns the Go language version for version x.
// If x is not a valid version, Lang returns the empty string.
// For example:
//
// Lang("go1.21rc2") = "go1.21"
// Lang("go1.21.2") = "go1.21"
// Lang("go1.21") = "go1.21"
// Lang("go1") = "go1"
// Lang("bad") = ""
// Lang("1.21") = ""
func Lang(x string) string {
v := gover.Lang(stripGo(x))
if v == "" {
return ""
}
return x[:2+len(v)] // "go"+v without allocation
}

// Compare returns -1, 0, or +1 depending on whether
// x < y, x == y, or x > y, interpreted as Go versions.
// The versions x and y must begin with a "go" prefix: "go1.21" not "1.21".
// Invalid versions, including the empty string, compare less than
// valid versions and equal to each other.
// The language version "go1.21" compares less than the
// release candidate and eventual releases "go1.21rc1" and "go1.21.0".
// Custom toolchain suffixes are ignored during comparison:
// "go1.21.0" and "go1.21.0-bigcorp" are equal.
func Compare(x, y string) int {
return gover.Compare(stripGo(x), stripGo(y))
}

// IsValid reports whether the version x is valid.
func IsValid(x string) bool {
return gover.IsValid(stripGo(x))
}
Loading

0 comments on commit b54cae2

Please sign in to comment.