Skip to content

Commit

Permalink
refactor: use gover from golang codebase
Browse files Browse the repository at this point in the history
and remove semver
  • Loading branch information
Zxilly committed Feb 10, 2024
1 parent 7b68ef2 commit 4c609c7
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 176 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
2. Import it into your project.
3. Write a new cool tool.

For an example use case, please checkout [redress](https://github.com/goretk/redress).
For an example use case, please check out [redress](https://github.com/goretk/redress).

### Sample code

Expand Down
6 changes: 5 additions & 1 deletion extern/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

This package holds code copied and modified from other
open source projects. Each file is licensed under its
original license.
original license.

## Structure

`gover/` - Copied from `Golang 1.23` `src/internal/gover`
223 changes: 223 additions & 0 deletions extern/gover/gover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
// 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 (
"cmp"
)

// 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
}

// 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
}

// 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 IsLang(x string) bool {
v := Parse(x)
return v != Version{} && v.Patch == "" && v.Kind == "" && v.Pre == ""
}

// 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 == "" || v.Major == "1" && v.Minor == "0" {
return v.Major
}
return v.Major + "." + 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 { // no digits or unnecessary leading zero
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)
}
138 changes: 138 additions & 0 deletions extern/gover/gover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// 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

import (
"reflect"
"testing"
)

func TestCompare(t *testing.T) { test2(t, compareTests, "Compare", Compare) }

var compareTests = []testCase2[string, string, int]{
{"", "", 0},
{"x", "x", 0},
{"", "x", 0},
{"1", "1.1", -1},
{"1.5", "1.6", -1},
{"1.5", "1.10", -1},
{"1.6", "1.6.1", -1},
{"1.19", "1.19.0", 0},
{"1.19rc1", "1.19", -1},
{"1.20", "1.20.0", 0},
{"1.20rc1", "1.20", -1},
{"1.21", "1.21.0", -1},
{"1.21", "1.21rc1", -1},
{"1.21rc1", "1.21.0", -1},
{"1.6", "1.19", -1},
{"1.19", "1.19.1", -1},
{"1.19rc1", "1.19", -1},
{"1.19rc1", "1.19.1", -1},
{"1.19rc1", "1.19rc2", -1},
{"1.19.0", "1.19.1", -1},
{"1.19rc1", "1.19.0", -1},
{"1.19alpha3", "1.19beta2", -1},
{"1.19beta2", "1.19rc1", -1},
{"1.1", "1.99999999999999998", -1},
{"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"},
{"1.999testmod", "1.999"},
}

func TestIsLang(t *testing.T) { test1(t, isLangTests, "IsLang", IsLang) }

var isLangTests = []testCase1[string, bool]{
{"1.2rc3", false},
{"1.2.3", false},
{"1.999testmod", false},
{"1.22", true},
{"1.21", true},
{"1.20", false}, // == 1.20.0
{"1.19", false}, // == 1.20.0
{"1.3", false}, // == 1.3.0
{"1.2", false}, // == 1.2.0
{"1", false}, // == 1.0.0
}

func TestIsValid(t *testing.T) { test1(t, isValidTests, "IsValid", IsValid) }

var isValidTests = []testCase1[string, bool]{
{"1.2rc3", true},
{"1.2.3", true},
{"1.999testmod", true},
{"1.600+auto", false},
{"1.22", true},
{"1.21.0", true},
{"1.21rc2", true},
{"1.21", true},
{"1.20.0", true},
{"1.20", true},
{"1.19", true},
{"1.3", true},
{"1.2", true},
{"1", true},
}

type testCase1[In, Out any] struct {
in In
out Out
}

type testCase2[In1, In2, Out any] struct {
in1 In1
in2 In2
out Out
}

type testCase3[In1, In2, In3, Out any] struct {
in1 In1
in2 In2
in3 In3
out Out
}

func test1[In, Out any](t *testing.T, tests []testCase1[In, Out], name string, f func(In) Out) {
t.Helper()
for _, tt := range tests {
if out := f(tt.in); !reflect.DeepEqual(out, tt.out) {
t.Errorf("%s(%v) = %v, want %v", name, tt.in, out, tt.out)
}
}
}

func test2[In1, In2, Out any](t *testing.T, tests []testCase2[In1, In2, Out], name string, f func(In1, In2) Out) {
t.Helper()
for _, tt := range tests {
if out := f(tt.in1, tt.in2); !reflect.DeepEqual(out, tt.out) {
t.Errorf("%s(%+v, %+v) = %+v, want %+v", name, tt.in1, tt.in2, out, tt.out)
}
}
}
Loading

0 comments on commit 4c609c7

Please sign in to comment.