fix: improve runtime version parsing (#4961)

This commit is contained in:
Ludovic Fernandez 2024-08-27 22:50:05 +02:00 committed by GitHub
parent 98b685cc0d
commit 54d089d106
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 238 additions and 178 deletions

View File

@ -1,11 +1,7 @@
package config
import (
"fmt"
"go/version"
"os"
"regexp"
"runtime"
"strings"
hcversion "github.com/hashicorp/go-version"
@ -92,56 +88,3 @@ func detectGoVersion() string {
return "1.17"
}
// Trims the Go version to keep only M.m.
// Since Go 1.21 the version inside the go.mod can be a patched version (ex: 1.21.0).
// The version can also include information which we want to remove (ex: 1.21alpha1)
// https://go.dev/doc/toolchain#versions
// This a problem with staticcheck and gocritic.
func trimGoVersion(v string) string {
if v == "" {
return ""
}
exp := regexp.MustCompile(`(\d\.\d+)(?:\.\d+|[a-z]+\d)`)
if exp.MatchString(v) {
return exp.FindStringSubmatch(v)[1]
}
return v
}
func getRuntimeGoVersion() string {
goVersion := runtime.Version()
parts := strings.Fields(goVersion)
if len(parts) == 0 {
return goVersion
}
// When using GOEXPERIMENT, the version returned might look something like "go1.23.0 X:boringcrypto".
return parts[0]
}
func checkGoVersion(goVersion string) error {
langVersion := version.Lang(getRuntimeGoVersion())
runtimeVersion, err := hcversion.NewVersion(strings.TrimPrefix(langVersion, "go"))
if err != nil {
return err
}
targetedVersion, err := hcversion.NewVersion(trimGoVersion(goVersion))
if err != nil {
return err
}
if runtimeVersion.LessThan(targetedVersion) {
return fmt.Errorf("the Go language version (%s) used to build golangci-lint is lower than the targeted Go version (%s)",
langVersion, goVersion)
}
return nil
}

View File

@ -4,7 +4,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIsGoGreaterThanOrEqual(t *testing.T) {
@ -84,104 +83,3 @@ func TestIsGoGreaterThanOrEqual(t *testing.T) {
})
}
}
func Test_trimGoVersion(t *testing.T) {
testCases := []struct {
desc string
version string
expected string
}{
{
desc: "patched version",
version: "1.22.0",
expected: "1.22",
},
{
desc: "minor version",
version: "1.22",
expected: "1.22",
},
{
desc: "RC version",
version: "1.22rc1",
expected: "1.22",
},
{
desc: "alpha version",
version: "1.22alpha1",
expected: "1.22",
},
{
desc: "beta version",
version: "1.22beta1",
expected: "1.22",
},
{
desc: "semver RC version",
version: "1.22.0-rc1",
expected: "1.22",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
version := trimGoVersion(test.version)
assert.Equal(t, test.expected, version)
})
}
}
func Test_checkGoVersion(t *testing.T) {
testCases := []struct {
desc string
version string
require require.ErrorAssertionFunc
}{
{
desc: "version greater than runtime version (patch)",
version: "1.30.1",
require: require.Error,
},
{
desc: "version greater than runtime version (family)",
version: "1.30",
require: require.Error,
},
{
desc: "version greater than runtime version (RC)",
version: "1.30.0-rc1",
require: require.Error,
},
{
desc: "version equals to runtime version",
version: getRuntimeGoVersion(),
require: require.NoError,
},
{
desc: "version lower than runtime version (patch)",
version: "1.19.1",
require: require.NoError,
},
{
desc: "version lower than runtime version (family)",
version: "1.19",
require: require.NoError,
},
{
desc: "version lower than runtime version (RC)",
version: "1.19.0-rc1",
require: require.NoError,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := checkGoVersion(test.version)
test.require(t, err)
})
}
}

View File

@ -14,6 +14,7 @@ import (
"github.com/golangci/golangci-lint/pkg/exitcodes"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/logutils"
)
@ -74,7 +75,7 @@ func (l *Loader) Load(opts LoadOptions) error {
l.handleGoVersion()
err = checkGoVersion(l.cfg.Run.Go)
err = goutil.CheckGoVersion(l.cfg.Run.Go)
if err != nil {
return err
}
@ -295,7 +296,7 @@ func (l *Loader) handleGoVersion() {
l.cfg.LintersSettings.Gofumpt.LangVersion = l.cfg.Run.Go
}
trimmedGoVersion := trimGoVersion(l.cfg.Run.Go)
trimmedGoVersion := goutil.TrimGoVersion(l.cfg.Run.Go)
l.cfg.LintersSettings.Revive.Go = trimmedGoVersion

View File

@ -9,8 +9,6 @@ import (
"go/types"
"os"
"reflect"
"runtime"
"strings"
"sync"
"sync/atomic"
@ -18,6 +16,7 @@ import (
"golang.org/x/tools/go/packages"
"github.com/golangci/golangci-lint/pkg/goanalysis/load"
"github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/logutils"
)
@ -153,12 +152,18 @@ func (lp *loadingPackage) loadFromSource(loadMode LoadMode) error {
return imp.Types, nil
}
// TODO(ldez) temporary workaround
rv, err := goutil.CleanRuntimeVersion()
if err != nil {
return err
}
tc := &types.Config{
Importer: importerFunc(importer),
Error: func(err error) {
pkg.Errors = append(pkg.Errors, lp.convertError(err)...)
},
GoVersion: getGoVersion(),
GoVersion: rv, // TODO(ldez) temporary workaround
}
_ = types.NewChecker(tc, pkg.Fset, pkg.Types, pkg.TypesInfo).Files(pkg.Syntax)
@ -502,17 +507,3 @@ func sizeOfReflectValueTreeBytes(rv reflect.Value, visitedPtrs map[uintptr]struc
panic("unknown rv of type " + rv.String())
}
}
// TODO(ldez) temporary workaround
func getGoVersion() string {
goVersion := runtime.Version()
parts := strings.Fields(goVersion)
if len(parts) == 0 {
return goVersion
}
// When using GOEXPERIMENT, the version returned might look something like "go1.23.0 X:boringcrypto".
return parts[0]
}

75
pkg/goutil/version.go Normal file
View File

@ -0,0 +1,75 @@
package goutil
import (
"fmt"
"go/version"
"regexp"
"runtime"
"strings"
hcversion "github.com/hashicorp/go-version"
)
func CheckGoVersion(goVersion string) error {
rv, err := CleanRuntimeVersion()
if err != nil {
return fmt.Errorf("clean runtime version: %w", err)
}
langVersion := version.Lang(rv)
runtimeVersion, err := hcversion.NewVersion(strings.TrimPrefix(langVersion, "go"))
if err != nil {
return err
}
targetedVersion, err := hcversion.NewVersion(TrimGoVersion(goVersion))
if err != nil {
return err
}
if runtimeVersion.LessThan(targetedVersion) {
return fmt.Errorf("the Go language version (%s) used to build golangci-lint is lower than the targeted Go version (%s)",
langVersion, goVersion)
}
return nil
}
// TrimGoVersion Trims the Go version to keep only M.m.
// Since Go 1.21 the version inside the go.mod can be a patched version (ex: 1.21.0).
// The version can also include information which we want to remove (ex: 1.21alpha1)
// https://go.dev/doc/toolchain#versions
// This a problem with staticcheck and gocritic.
func TrimGoVersion(v string) string {
if v == "" {
return ""
}
exp := regexp.MustCompile(`(\d\.\d+)(?:\.\d+|[a-z]+\d)`)
if exp.MatchString(v) {
return exp.FindStringSubmatch(v)[1]
}
return v
}
func CleanRuntimeVersion() (string, error) {
return cleanRuntimeVersion(runtime.Version())
}
func cleanRuntimeVersion(rv string) (string, error) {
parts := strings.Fields(rv)
for _, part := range parts {
// Allow to handle:
// - GOEXPERIMENT -> "go1.23.0 X:boringcrypto"
// - devel -> "devel go1.24-e705a2d Wed Aug 7 01:16:42 2024 +0000 linux/amd64"
if strings.HasPrefix(part, "go1.") {
return part, nil
}
}
return "", fmt.Errorf("invalid Go runtime version: %s", rv)
}

152
pkg/goutil/version_test.go Normal file
View File

@ -0,0 +1,152 @@
package goutil
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCheckGoVersion(t *testing.T) {
testCases := []struct {
desc string
version string
require require.ErrorAssertionFunc
}{
{
desc: "version greater than runtime version (patch)",
version: "1.30.1",
require: require.Error,
},
{
desc: "version greater than runtime version (family)",
version: "1.30",
require: require.Error,
},
{
desc: "version greater than runtime version (RC)",
version: "1.30.0-rc1",
require: require.Error,
},
{
desc: "version equals to runtime version",
version: func() string {
rv, _ := CleanRuntimeVersion()
return rv
}(),
require: require.NoError,
},
{
desc: "version lower than runtime version (patch)",
version: "1.19.1",
require: require.NoError,
},
{
desc: "version lower than runtime version (family)",
version: "1.19",
require: require.NoError,
},
{
desc: "version lower than runtime version (RC)",
version: "1.19.0-rc1",
require: require.NoError,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := CheckGoVersion(test.version)
test.require(t, err)
})
}
}
func TestTrimGoVersion(t *testing.T) {
testCases := []struct {
desc string
version string
expected string
}{
{
desc: "patched version",
version: "1.22.0",
expected: "1.22",
},
{
desc: "minor version",
version: "1.22",
expected: "1.22",
},
{
desc: "RC version",
version: "1.22rc1",
expected: "1.22",
},
{
desc: "alpha version",
version: "1.22alpha1",
expected: "1.22",
},
{
desc: "beta version",
version: "1.22beta1",
expected: "1.22",
},
{
desc: "semver RC version",
version: "1.22.0-rc1",
expected: "1.22",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
version := TrimGoVersion(test.version)
assert.Equal(t, test.expected, version)
})
}
}
func Test_cleanRuntimeVersion(t *testing.T) {
testCases := []struct {
desc string
version string
expected string
}{
{
desc: "go version",
version: "go1.22.0",
expected: "go1.22.0",
},
{
desc: "language version",
version: "go1.22",
expected: "go1.22",
},
{
desc: "GOEXPERIMENT",
version: "go1.23.0 X:boringcrypto",
expected: "go1.23.0",
},
{
desc: "devel",
version: "devel go1.24-e705a2d Wed Aug 7 01:16:42 2024 +0000 linux/amd64",
expected: "go1.24-e705a2d",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
v, err := cleanRuntimeVersion(test.version)
require.NoError(t, err)
assert.Equal(t, test.expected, v)
})
}
}