diff --git a/internal/versions/gover.go b/internal/versions/gover.go new file mode 100644 index 000000000..bbabcd22e --- /dev/null +++ b/internal/versions/gover.go @@ -0,0 +1,172 @@ +// 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. + +// This is a fork of internal/gover for use by x/tools until +// go1.21 and earlier are no longer supported by x/tools. + +package versions + +import "strings" + +// A gover is a parsed Go gover: 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 gover 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 := strings.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 +} + +// 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) != gover{} +} + +// parse parses the Go version string x into a version. +// It returns the zero version if x is malformed. +func parse(x string) gover { + var v gover + + // Parse major version. + var ok bool + v.major, x, ok = cutInt(x) + if !ok { + return gover{} + } + if x == "" { + // Interpret "1" as "1.0.0". + v.minor = "0" + v.patch = "0" + return v + } + + // Parse . before minor version. + if x[0] != '.' { + return gover{} + } + + // Parse minor version. + v.minor, x, ok = cutInt(x[1:]) + if !ok { + return gover{} + } + 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 gover{} + } + return v + } + + // Parse prerelease. + i := 0 + for i < len(x) && (x[i] < '0' || '9' < x[i]) { + if x[i] < 'a' || 'z' < x[i] { + return gover{} + } + i++ + } + if i == 0 { + return gover{} + } + v.kind, x = x[:i], x[i:] + if x == "" { + return v + } + v.pre, x, ok = cutInt(x) + if !ok || x != "" { + return gover{} + } + + 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 + } +} diff --git a/internal/versions/types.go b/internal/versions/types.go new file mode 100644 index 000000000..562eef21f --- /dev/null +++ b/internal/versions/types.go @@ -0,0 +1,19 @@ +// 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 versions + +import ( + "go/types" +) + +// GoVersion returns the Go version of the type package. +// It returns zero if no version can be determined. +func GoVersion(pkg *types.Package) string { + // TODO(taking): x/tools can call GoVersion() [from 1.21] after 1.25. + if pkg, ok := any(pkg).(interface{ GoVersion() string }); ok { + return pkg.GoVersion() + } + return "" +} diff --git a/internal/versions/types_go121.go b/internal/versions/types_go121.go new file mode 100644 index 000000000..a7b79207a --- /dev/null +++ b/internal/versions/types_go121.go @@ -0,0 +1,20 @@ +// 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. + +//go:build !go1.22 +// +build !go1.22 + +package versions + +import ( + "go/ast" + "go/types" +) + +// FileVersions always reports the a file's Go version as the +// zero version at this Go version. +func FileVersions(info *types.Info, file *ast.File) string { return "" } + +// InitFileVersions is a noop at this Go version. +func InitFileVersions(*types.Info) {} diff --git a/internal/versions/types_go122.go b/internal/versions/types_go122.go new file mode 100644 index 000000000..7b9ba89a8 --- /dev/null +++ b/internal/versions/types_go122.go @@ -0,0 +1,24 @@ +// 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. + +//go:build go1.22 +// +build go1.22 + +package versions + +import ( + "go/ast" + "go/types" +) + +// FileVersions maps a file to the file's semantic Go version. +// The reported version is the zero version if a version cannot be determined. +func FileVersions(info *types.Info, file *ast.File) string { + return info.FileVersions[file] +} + +// InitFileVersions initializes info to record Go versions for Go files. +func InitFileVersions(info *types.Info) { + info.FileVersions = make(map[*ast.File]string) +} diff --git a/internal/versions/types_test.go b/internal/versions/types_test.go new file mode 100644 index 000000000..0ffdd468d --- /dev/null +++ b/internal/versions/types_test.go @@ -0,0 +1,87 @@ +// 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 versions_test + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/versions" +) + +func Test(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + var contents = map[string]string{ + "gobuild.go": ` + //go:build go1.23 + package p + `, + "noversion.go": ` + package p + `, + } + type fileTest struct { + fname string + want string + } + for _, item := range []struct { + goversion string + pversion string + tests []fileTest + }{ + {"", "", []fileTest{{"noversion.go", ""}, {"gobuild.go", ""}}}, + {"go1.22", "go1.22", []fileTest{{"noversion.go", "go1.22"}, {"gobuild.go", "go1.23"}}}, + } { + name := fmt.Sprintf("types.Config{GoVersion:%q}", item.goversion) + t.Run(name, func(t *testing.T) { + fset := token.NewFileSet() + files := make([]*ast.File, len(item.tests)) + for i, test := range item.tests { + files[i] = parse(t, fset, test.fname, contents[test.fname]) + } + pkg, info := typeCheck(t, fset, files, item.goversion) + if got, want := versions.GoVersion(pkg), item.pversion; versions.Compare(got, want) != 0 { + t.Errorf("GoVersion()=%q. expected %q", got, want) + } + if got := versions.FileVersions(info, nil); got != "" { + t.Errorf(`FileVersions(nil)=%q. expected ""`, got) + } + for i, test := range item.tests { + if got, want := versions.FileVersions(info, files[i]), test.want; got != want { + t.Errorf("FileVersions(%s)=%q. expected %q", test.fname, got, want) + } + } + }) + } +} + +func parse(t *testing.T, fset *token.FileSet, name, src string) *ast.File { + file, err := parser.ParseFile(fset, name, src, 0) + if err != nil { + t.Fatal(err) + } + return file +} + +func typeCheck(t *testing.T, fset *token.FileSet, files []*ast.File, goversion string) (*types.Package, *types.Info) { + conf := types.Config{ + Importer: importer.Default(), + GoVersion: goversion, + } + info := types.Info{} + versions.InitFileVersions(&info) + pkg, err := conf.Check("", fset, files, &info) + if err != nil { + t.Fatal(err) + } + return pkg, &info +} diff --git a/internal/versions/versions_go121.go b/internal/versions/versions_go121.go new file mode 100644 index 000000000..dfbc8b8ab --- /dev/null +++ b/internal/versions/versions_go121.go @@ -0,0 +1,34 @@ +// 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. + +//go:build !go1.22 +// +build !go1.22 + +package versions + +// 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 { return lang(x) } + +// 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 compare(x, y) } + +// IsValid reports whether the version x is valid. +func IsValid(x string) bool { return isValid(x) } diff --git a/internal/versions/versions_go122.go b/internal/versions/versions_go122.go new file mode 100644 index 000000000..c1c1814b2 --- /dev/null +++ b/internal/versions/versions_go122.go @@ -0,0 +1,38 @@ +// 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. + +//go:build go1.22 +// +build go1.22 + +package versions + +import ( + "go/version" +) + +// 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 { return version.Lang(x) } + +// 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 version.Compare(x, y) } + +// IsValid reports whether the version x is valid. +func IsValid(x string) bool { return version.IsValid(x) } diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go new file mode 100644 index 000000000..fee6512ed --- /dev/null +++ b/internal/versions/versions_test.go @@ -0,0 +1,137 @@ +// 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 versions_test + +import ( + "testing" + + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/versions" +) + +func TestIsValid(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + // valid versions + for _, x := range []string{ + "go1.21", + "go1.21.2", + "go1.21rc", + "go1.21rc2", + "go0.0", // ?? + "go1", + "go2", + } { + if !versions.IsValid(x) { + t.Errorf("expected versions.IsValid(%q) to hold", x) + } + } + + // invalid versions + for _, x := range []string{ + "", + "bad", + "1.21", + "v1.21", + "go", + "goAA", + "go2_3", + "go1.BB", + "go1.21.", + "go1.21.2_2", + "go1.21rc_2", + "go1.21rc2_", + } { + if versions.IsValid(x) { + t.Errorf("expected versions.IsValid(%q) to not hold", x) + } + } +} + +func TestVersionComparisons(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + for _, item := range []struct { + x, y string + want int + }{ + {"go2", "go2", 0}, + {"go2", "go1.21.2", +1}, + {"go2", "go1.21rc2", +1}, + {"go2", "go1.21rc", +1}, + {"go2", "go1.21", +1}, + {"go2", "go1", +1}, + {"go2", "go0.0", +1}, + {"go2", "", +1}, + {"go2", "bad", +1}, + {"go1.21.2", "go1.21.2", 0}, + {"go1.21.2", "go1.21rc2", +1}, + {"go1.21.2", "go1.21rc", +1}, + {"go1.21.2", "go1.21", +1}, + {"go1.21.2", "go1", +1}, + {"go1.21.2", "go0.0", +1}, + {"go1.21.2", "", +1}, + {"go1.21.2", "bad", +1}, + {"go1.21rc2", "go1.21rc2", 0}, + {"go1.21rc2", "go1.21rc", +1}, + {"go1.21rc2", "go1.21", +1}, + {"go1.21rc2", "go1", +1}, + {"go1.21rc2", "go0.0", +1}, + {"go1.21rc2", "", +1}, + {"go1.21rc2", "bad", +1}, + {"go1.21rc", "go1.21rc", 0}, + {"go1.21rc", "go1.21", +1}, + {"go1.21rc", "go1", +1}, + {"go1.21rc", "go0.0", +1}, + {"go1.21rc", "", +1}, + {"go1.21rc", "bad", +1}, + {"go1.21", "go1.21", 0}, + {"go1.21", "go1", +1}, + {"go1.21", "go0.0", +1}, + {"go1.21", "", +1}, + {"go1.21", "bad", +1}, + {"go1", "go1", 0}, + {"go1", "go0.0", +1}, + {"go1", "", +1}, + {"go1", "bad", +1}, + {"go0.0", "go0.0", 0}, + {"go0.0", "", +1}, + {"go0.0", "bad", +1}, + {"", "", 0}, + {"", "bad", 0}, + {"bad", "bad", 0}, + } { + got := versions.Compare(item.x, item.y) + if got != item.want { + t.Errorf("versions.Compare(%q, %q)=%d. expected %d", item.x, item.y, got, item.want) + } + reverse := versions.Compare(item.y, item.x) + if reverse != -got { + t.Errorf("versions.Compare(%q, %q)=%d. expected %d", item.y, item.x, reverse, -got) + } + } +} + +func TestLang(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + for _, item := range []struct { + x string + want string + }{ + // valid + {"go1.21rc2", "go1.21"}, + {"go1.21.2", "go1.21"}, + {"go1.21", "go1.21"}, + {"go1", "go1"}, + // invalid + {"bad", ""}, + {"1.21", ""}, + } { + if got := versions.Lang(item.x); got != item.want { + t.Errorf("versions.Lang(%q)=%q. expected %q", item.x, got, item.want) + } + } + +}