Skip to content

Commit

Permalink
internal/versions: add a new versions package
Browse files Browse the repository at this point in the history
Adds a new versions package to provide x/tools a way to
deal with new GoVersion() and FileVersions API from go/types
and the new go/version standard library.

This provides a stable API until 1.26.

Updates golang/go#63374
Updates golang/go#62605

Change-Id: I4de54df00ea0f4363c0383cbdc917186277bfd0a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/533056
Run-TryBot: Tim King <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Alan Donovan <[email protected]>
TryBot-Result: Gopher Robot <[email protected]>
  • Loading branch information
timothy-king committed Nov 10, 2023
1 parent 4749c1b commit 3b6876f
Show file tree
Hide file tree
Showing 8 changed files with 531 additions and 0 deletions.
172 changes: 172 additions & 0 deletions internal/versions/gover.go
Original file line number Diff line number Diff line change
@@ -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
}
}
19 changes: 19 additions & 0 deletions internal/versions/types.go
Original file line number Diff line number Diff line change
@@ -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 ""
}
20 changes: 20 additions & 0 deletions internal/versions/types_go121.go
Original file line number Diff line number Diff line change
@@ -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) {}
24 changes: 24 additions & 0 deletions internal/versions/types_go122.go
Original file line number Diff line number Diff line change
@@ -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)
}
87 changes: 87 additions & 0 deletions internal/versions/types_test.go
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions internal/versions/versions_go121.go
Original file line number Diff line number Diff line change
@@ -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) }
Loading

0 comments on commit 3b6876f

Please sign in to comment.