go/types: record Config.GoVersion for reporting in Package.GoVersion method

Clients of go/types, such as analyzers, may need to know which
specific Go version a package is written for. Record that information
in the Package and expose it using the new GoVersion method.

Update parseGoVersion to handle the new Go versions that may
be passed around starting in Go 1.21.0: versions like "go1.21.0"
and "go1.21rc2". This is not strictly necessary today, but it adds some
valuable future-proofing.

While we are here, change NewChecker from panicking on invalid
version to saving an error for returning later from Files.
Go versions are now likely to be coming from a variety of sources,
not just hard-coded in calls to NewChecker, making a panic
inappropriate.

For #61174.
Fixes #61175.

Change-Id: Ibe41fe207c1b6e71064b1fe448ac55776089c541
Reviewed-on: https://go-review.googlesource.com/c/go/+/507975
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Bryan Mills <bcmills@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Russ Cox 2023-07-05 12:08:51 -04:00
parent 36ea4f9680
commit b490bdc27d
10 changed files with 105 additions and 55 deletions

View File

@ -174,6 +174,7 @@ pkg go/build, type Package struct, Directives []Directive #56986
pkg go/build, type Package struct, TestDirectives []Directive #56986 pkg go/build, type Package struct, TestDirectives []Directive #56986
pkg go/build, type Package struct, XTestDirectives []Directive #56986 pkg go/build, type Package struct, XTestDirectives []Directive #56986
pkg go/token, method (*File) Lines() []int #57708 pkg go/token, method (*File) Lines() []int #57708
pkg go/types, method (*Package) GoVersion() string #61175
pkg html/template, const ErrJSTemplate = 12 #59584 pkg html/template, const ErrJSTemplate = 12 #59584
pkg html/template, const ErrJSTemplate ErrorCode #59584 pkg html/template, const ErrJSTemplate ErrorCode #59584
pkg io/fs, func FormatDirEntry(DirEntry) string #54451 pkg io/fs, func FormatDirEntry(DirEntry) string #54451

View File

@ -10,13 +10,14 @@ import (
// A Package describes a Go package. // A Package describes a Go package.
type Package struct { type Package struct {
path string path string
name string name string
scope *Scope scope *Scope
imports []*Package imports []*Package
complete bool complete bool
fake bool // scope lookup errors are silently dropped if package is fake (internal use only) fake bool // scope lookup errors are silently dropped if package is fake (internal use only)
cgo bool // uses of this package will be rewritten into uses of declarations from _cgo_gotypes.go cgo bool // uses of this package will be rewritten into uses of declarations from _cgo_gotypes.go
goVersion string // minimum Go version required for package (by Config.GoVersion, typically from go.mod)
} }
// NewPackage returns a new Package for the given package path and name. // NewPackage returns a new Package for the given package path and name.
@ -35,6 +36,12 @@ func (pkg *Package) Name() string { return pkg.name }
// SetName sets the package name. // SetName sets the package name.
func (pkg *Package) SetName(name string) { pkg.name = name } func (pkg *Package) SetName(name string) { pkg.name = name }
// GoVersion returns the minimum Go version required by this package.
// If the minimum version is unknown, GoVersion returns the empty string.
// Individual source files may specify a different minimum Go version,
// as reported in the [go/ast.File.GoVersion] field.
func (pkg *Package) GoVersion() string { return pkg.goVersion }
// Scope returns the (complete or incomplete) package scope // Scope returns the (complete or incomplete) package scope
// holding the objects declared at package level (TypeNames, // holding the objects declared at package level (TypeNames,
// Consts, Vars, and Funcs). // Consts, Vars, and Funcs).

View File

@ -47,7 +47,7 @@ func TestSizeof(t *testing.T) {
// Misc // Misc
{Scope{}, 60, 104}, {Scope{}, 60, 104},
{Package{}, 36, 72}, {Package{}, 44, 88},
{_TypeSet{}, 28, 56}, {_TypeSet{}, 28, 56},
} }

View File

@ -6,7 +6,6 @@ package types2
import ( import (
"cmd/compile/internal/syntax" "cmd/compile/internal/syntax"
"errors"
"fmt" "fmt"
"strings" "strings"
) )
@ -44,23 +43,24 @@ var (
go1_21 = version{1, 21} go1_21 = version{1, 21}
) )
var errVersionSyntax = errors.New("invalid Go version syntax")
// parseGoVersion parses a Go version string (such as "go1.12") // parseGoVersion parses a Go version string (such as "go1.12")
// and returns the version, or an error. If s is the empty // and returns the version, or an error. If s is the empty
// string, the version is 0.0. // string, the version is 0.0.
func parseGoVersion(s string) (v version, err error) { func parseGoVersion(s string) (v version, err error) {
bad := func() (version, error) {
return version{}, fmt.Errorf("invalid Go version syntax %q", s)
}
if s == "" { if s == "" {
return return
} }
if !strings.HasPrefix(s, "go") { if !strings.HasPrefix(s, "go") {
return version{}, errVersionSyntax return bad()
} }
s = s[len("go"):] s = s[len("go"):]
i := 0 i := 0
for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
if i >= 10 || i == 0 && s[i] == '0' { if i >= 10 || i == 0 && s[i] == '0' {
return version{}, errVersionSyntax return bad()
} }
v.major = 10*v.major + int(s[i]) - '0' v.major = 10*v.major + int(s[i]) - '0'
} }
@ -68,7 +68,7 @@ func parseGoVersion(s string) (v version, err error) {
return return
} }
if i == 0 || s[i] != '.' { if i == 0 || s[i] != '.' {
return version{}, errVersionSyntax return bad()
} }
s = s[i+1:] s = s[i+1:]
if s == "0" { if s == "0" {
@ -81,14 +81,15 @@ func parseGoVersion(s string) (v version, err error) {
i = 0 i = 0
for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
if i >= 10 || i == 0 && s[i] == '0' { if i >= 10 || i == 0 && s[i] == '0' {
return version{}, errVersionSyntax return bad()
} }
v.minor = 10*v.minor + int(s[i]) - '0' v.minor = 10*v.minor + int(s[i]) - '0'
} }
if i > 0 && i == len(s) { // Accept any suffix after the minor number.
return // We are only looking for the language version (major.minor)
} // but want to accept any valid Go version, like go1.21.0
return version{}, errVersionSyntax // and go1.21rc2.
return
} }
// langCompat reports an error if the representation of a numeric // langCompat reports an error if the representation of a numeric

View File

@ -286,7 +286,7 @@ var depsRules = `
math/big, go/token math/big, go/token
< go/constant; < go/constant;
container/heap, go/constant, go/parser, internal/types/errors container/heap, go/constant, go/parser, internal/goversion, internal/types/errors
< go/types; < go/types;
# The vast majority of standard library packages should not be resorting to regexp. # The vast majority of standard library packages should not be resorting to regexp.

View File

@ -12,6 +12,7 @@ import (
"go/ast" "go/ast"
"go/constant" "go/constant"
"go/token" "go/token"
"internal/goversion"
. "internal/types/errors" . "internal/types/errors"
) )
@ -98,11 +99,12 @@ type Checker struct {
fset *token.FileSet fset *token.FileSet
pkg *Package pkg *Package
*Info *Info
version version // accepted language version version version // accepted language version
nextID uint64 // unique Id for type parameters (first valid Id is 1) versionErr error // version error, delayed from NewChecker
objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info nextID uint64 // unique Id for type parameters (first valid Id is 1)
impMap map[importKey]*Package // maps (import path, source directory) to (complete or fake) package objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info
valids instanceLookup // valid *Named (incl. instantiated) types per the validType check impMap map[importKey]*Package // maps (import path, source directory) to (complete or fake) package
valids instanceLookup // valid *Named (incl. instantiated) types per the validType check
// pkgPathMap maps package names to the set of distinct import paths we've // pkgPathMap maps package names to the set of distinct import paths we've
// seen for that name, anywhere in the import graph. It is used for // seen for that name, anywhere in the import graph. It is used for
@ -233,20 +235,21 @@ func NewChecker(conf *Config, fset *token.FileSet, pkg *Package, info *Info) *Ch
info = new(Info) info = new(Info)
} }
version, err := parseGoVersion(conf.GoVersion) version, versionErr := parseGoVersion(conf.GoVersion)
if err != nil { if pkg != nil {
panic(fmt.Sprintf("invalid Go version %q (%v)", conf.GoVersion, err)) pkg.goVersion = conf.GoVersion
} }
return &Checker{ return &Checker{
conf: conf, conf: conf,
ctxt: conf.Context, ctxt: conf.Context,
fset: fset, fset: fset,
pkg: pkg, pkg: pkg,
Info: info, Info: info,
version: version, version: version,
objMap: make(map[Object]*declInfo), versionErr: versionErr,
impMap: make(map[importKey]*Package), objMap: make(map[Object]*declInfo),
impMap: make(map[importKey]*Package),
} }
} }
@ -342,6 +345,12 @@ func (check *Checker) Files(files []*ast.File) error { return check.checkFiles(f
var errBadCgo = errors.New("cannot use FakeImportC and go115UsesCgo together") var errBadCgo = errors.New("cannot use FakeImportC and go115UsesCgo together")
func (check *Checker) checkFiles(files []*ast.File) (err error) { func (check *Checker) checkFiles(files []*ast.File) (err error) {
if check.versionErr != nil {
return check.versionErr
}
if check.version.after(version{1, goversion.Version}) {
return fmt.Errorf("package requires newer Go version %v", check.version)
}
if check.conf.FakeImportC && check.conf.go115UsesCgo { if check.conf.FakeImportC && check.conf.go115UsesCgo {
return errBadCgo return errBadCgo
} }

View File

@ -12,13 +12,14 @@ import (
// A Package describes a Go package. // A Package describes a Go package.
type Package struct { type Package struct {
path string path string
name string name string
scope *Scope scope *Scope
imports []*Package imports []*Package
complete bool complete bool
fake bool // scope lookup errors are silently dropped if package is fake (internal use only) fake bool // scope lookup errors are silently dropped if package is fake (internal use only)
cgo bool // uses of this package will be rewritten into uses of declarations from _cgo_gotypes.go cgo bool // uses of this package will be rewritten into uses of declarations from _cgo_gotypes.go
goVersion string // minimum Go version required for package (by Config.GoVersion, typically from go.mod)
} }
// NewPackage returns a new Package for the given package path and name. // NewPackage returns a new Package for the given package path and name.
@ -37,6 +38,12 @@ func (pkg *Package) Name() string { return pkg.name }
// SetName sets the package name. // SetName sets the package name.
func (pkg *Package) SetName(name string) { pkg.name = name } func (pkg *Package) SetName(name string) { pkg.name = name }
// GoVersion returns the minimum Go version required by this package.
// If the minimum version is unknown, GoVersion returns the empty string.
// Individual source files may specify a different minimum Go version,
// as reported in the [go/ast.File.GoVersion] field.
func (pkg *Package) GoVersion() string { return pkg.goVersion }
// Scope returns the (complete or incomplete) package scope // Scope returns the (complete or incomplete) package scope
// holding the objects declared at package level (TypeNames, // holding the objects declared at package level (TypeNames,
// Consts, Vars, and Funcs). // Consts, Vars, and Funcs).

View File

@ -46,7 +46,7 @@ func TestSizeof(t *testing.T) {
// Misc // Misc
{Scope{}, 44, 88}, {Scope{}, 44, 88},
{Package{}, 36, 72}, {Package{}, 44, 88},
{_TypeSet{}, 28, 56}, {_TypeSet{}, 28, 56},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -5,7 +5,6 @@
package types package types
import ( import (
"errors"
"fmt" "fmt"
"go/ast" "go/ast"
"go/token" "go/token"
@ -45,23 +44,24 @@ var (
go1_21 = version{1, 21} go1_21 = version{1, 21}
) )
var errVersionSyntax = errors.New("invalid Go version syntax")
// parseGoVersion parses a Go version string (such as "go1.12") // parseGoVersion parses a Go version string (such as "go1.12")
// and returns the version, or an error. If s is the empty // and returns the version, or an error. If s is the empty
// string, the version is 0.0. // string, the version is 0.0.
func parseGoVersion(s string) (v version, err error) { func parseGoVersion(s string) (v version, err error) {
bad := func() (version, error) {
return version{}, fmt.Errorf("invalid Go version syntax %q", s)
}
if s == "" { if s == "" {
return return
} }
if !strings.HasPrefix(s, "go") { if !strings.HasPrefix(s, "go") {
return version{}, errVersionSyntax return bad()
} }
s = s[len("go"):] s = s[len("go"):]
i := 0 i := 0
for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
if i >= 10 || i == 0 && s[i] == '0' { if i >= 10 || i == 0 && s[i] == '0' {
return version{}, errVersionSyntax return bad()
} }
v.major = 10*v.major + int(s[i]) - '0' v.major = 10*v.major + int(s[i]) - '0'
} }
@ -69,7 +69,7 @@ func parseGoVersion(s string) (v version, err error) {
return return
} }
if i == 0 || s[i] != '.' { if i == 0 || s[i] != '.' {
return version{}, errVersionSyntax return bad()
} }
s = s[i+1:] s = s[i+1:]
if s == "0" { if s == "0" {
@ -82,14 +82,15 @@ func parseGoVersion(s string) (v version, err error) {
i = 0 i = 0
for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
if i >= 10 || i == 0 && s[i] == '0' { if i >= 10 || i == 0 && s[i] == '0' {
return version{}, errVersionSyntax return bad()
} }
v.minor = 10*v.minor + int(s[i]) - '0' v.minor = 10*v.minor + int(s[i]) - '0'
} }
if i > 0 && i == len(s) { // Accept any suffix after the minor number.
return // We are only looking for the language version (major.minor)
} // but want to accept any valid Go version, like go1.21.0
return version{}, errVersionSyntax // and go1.21rc2.
return
} }
// langCompat reports an error if the representation of a numeric // langCompat reports an error if the representation of a numeric

View File

@ -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.
package types
import "testing"
var parseGoVersionTests = []struct {
in string
out version
}{
{"go1.21", version{1, 21}},
{"go1.21.0", version{1, 21}},
{"go1.21rc2", version{1, 21}},
}
func TestParseGoVersion(t *testing.T) {
for _, tt := range parseGoVersionTests {
if out, err := parseGoVersion(tt.in); out != tt.out || err != nil {
t.Errorf("parseGoVersion(%q) = %v, %v, want %v, nil", tt.in, out, err, tt.out)
}
}
}