diff --git a/api/go1.21.txt b/api/go1.21.txt index 6435d10914..c8ca3df2e6 100644 --- a/api/go1.21.txt +++ b/api/go1.21.txt @@ -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, XTestDirectives []Directive #56986 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 ErrorCode #59584 pkg io/fs, func FormatDirEntry(DirEntry) string #54451 diff --git a/src/cmd/compile/internal/types2/package.go b/src/cmd/compile/internal/types2/package.go index 61670f6718..e08099d81f 100644 --- a/src/cmd/compile/internal/types2/package.go +++ b/src/cmd/compile/internal/types2/package.go @@ -10,13 +10,14 @@ import ( // A Package describes a Go package. type Package struct { - path string - name string - scope *Scope - imports []*Package - complete bool - 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 + path string + name string + scope *Scope + imports []*Package + complete bool + 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 + 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. @@ -35,6 +36,12 @@ func (pkg *Package) Name() string { return pkg.name } // SetName sets the package 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 // holding the objects declared at package level (TypeNames, // Consts, Vars, and Funcs). diff --git a/src/cmd/compile/internal/types2/sizeof_test.go b/src/cmd/compile/internal/types2/sizeof_test.go index af82b3fa7a..740dbc9276 100644 --- a/src/cmd/compile/internal/types2/sizeof_test.go +++ b/src/cmd/compile/internal/types2/sizeof_test.go @@ -47,7 +47,7 @@ func TestSizeof(t *testing.T) { // Misc {Scope{}, 60, 104}, - {Package{}, 36, 72}, + {Package{}, 44, 88}, {_TypeSet{}, 28, 56}, } diff --git a/src/cmd/compile/internal/types2/version.go b/src/cmd/compile/internal/types2/version.go index 7d01b829a9..e525f16470 100644 --- a/src/cmd/compile/internal/types2/version.go +++ b/src/cmd/compile/internal/types2/version.go @@ -6,7 +6,6 @@ package types2 import ( "cmd/compile/internal/syntax" - "errors" "fmt" "strings" ) @@ -44,23 +43,24 @@ var ( go1_21 = version{1, 21} ) -var errVersionSyntax = errors.New("invalid Go version syntax") - // parseGoVersion parses a Go version string (such as "go1.12") // and returns the version, or an error. If s is the empty // string, the version is 0.0. func parseGoVersion(s string) (v version, err error) { + bad := func() (version, error) { + return version{}, fmt.Errorf("invalid Go version syntax %q", s) + } if s == "" { return } if !strings.HasPrefix(s, "go") { - return version{}, errVersionSyntax + return bad() } s = s[len("go"):] i := 0 for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { if i >= 10 || i == 0 && s[i] == '0' { - return version{}, errVersionSyntax + return bad() } v.major = 10*v.major + int(s[i]) - '0' } @@ -68,7 +68,7 @@ func parseGoVersion(s string) (v version, err error) { return } if i == 0 || s[i] != '.' { - return version{}, errVersionSyntax + return bad() } s = s[i+1:] if s == "0" { @@ -81,14 +81,15 @@ func parseGoVersion(s string) (v version, err error) { i = 0 for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { if i >= 10 || i == 0 && s[i] == '0' { - return version{}, errVersionSyntax + return bad() } v.minor = 10*v.minor + int(s[i]) - '0' } - if i > 0 && i == len(s) { - return - } - return version{}, errVersionSyntax + // Accept any suffix after the minor number. + // We are only looking for the language version (major.minor) + // but want to accept any valid Go version, like go1.21.0 + // and go1.21rc2. + return } // langCompat reports an error if the representation of a numeric diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index be8ac30f9d..2f335068b8 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -286,7 +286,7 @@ var depsRules = ` math/big, go/token < go/constant; - container/heap, go/constant, go/parser, internal/types/errors + container/heap, go/constant, go/parser, internal/goversion, internal/types/errors < go/types; # The vast majority of standard library packages should not be resorting to regexp. diff --git a/src/go/types/check.go b/src/go/types/check.go index 5381b5db68..591de5f329 100644 --- a/src/go/types/check.go +++ b/src/go/types/check.go @@ -12,6 +12,7 @@ import ( "go/ast" "go/constant" "go/token" + "internal/goversion" . "internal/types/errors" ) @@ -98,11 +99,12 @@ type Checker struct { fset *token.FileSet pkg *Package *Info - version version // accepted language version - nextID uint64 // unique Id for type parameters (first valid Id is 1) - objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info - 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 + version version // accepted language version + versionErr error // version error, delayed from NewChecker + nextID uint64 // unique Id for type parameters (first valid Id is 1) + objMap map[Object]*declInfo // maps package-level objects and (non-interface) methods to declaration info + 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 // 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) } - version, err := parseGoVersion(conf.GoVersion) - if err != nil { - panic(fmt.Sprintf("invalid Go version %q (%v)", conf.GoVersion, err)) + version, versionErr := parseGoVersion(conf.GoVersion) + if pkg != nil { + pkg.goVersion = conf.GoVersion } return &Checker{ - conf: conf, - ctxt: conf.Context, - fset: fset, - pkg: pkg, - Info: info, - version: version, - objMap: make(map[Object]*declInfo), - impMap: make(map[importKey]*Package), + conf: conf, + ctxt: conf.Context, + fset: fset, + pkg: pkg, + Info: info, + version: version, + versionErr: versionErr, + 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") 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 { return errBadCgo } diff --git a/src/go/types/package.go b/src/go/types/package.go index 7aa62fb7a3..0f52d5f489 100644 --- a/src/go/types/package.go +++ b/src/go/types/package.go @@ -12,13 +12,14 @@ import ( // A Package describes a Go package. type Package struct { - path string - name string - scope *Scope - imports []*Package - complete bool - 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 + path string + name string + scope *Scope + imports []*Package + complete bool + 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 + 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. @@ -37,6 +38,12 @@ func (pkg *Package) Name() string { return pkg.name } // SetName sets the package 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 // holding the objects declared at package level (TypeNames, // Consts, Vars, and Funcs). diff --git a/src/go/types/sizeof_test.go b/src/go/types/sizeof_test.go index f17a1781f5..9e5b5f8b20 100644 --- a/src/go/types/sizeof_test.go +++ b/src/go/types/sizeof_test.go @@ -46,7 +46,7 @@ func TestSizeof(t *testing.T) { // Misc {Scope{}, 44, 88}, - {Package{}, 36, 72}, + {Package{}, 44, 88}, {_TypeSet{}, 28, 56}, } for _, test := range tests { diff --git a/src/go/types/version.go b/src/go/types/version.go index 07a42a79ee..108d9b34a0 100644 --- a/src/go/types/version.go +++ b/src/go/types/version.go @@ -5,7 +5,6 @@ package types import ( - "errors" "fmt" "go/ast" "go/token" @@ -45,23 +44,24 @@ var ( go1_21 = version{1, 21} ) -var errVersionSyntax = errors.New("invalid Go version syntax") - // parseGoVersion parses a Go version string (such as "go1.12") // and returns the version, or an error. If s is the empty // string, the version is 0.0. func parseGoVersion(s string) (v version, err error) { + bad := func() (version, error) { + return version{}, fmt.Errorf("invalid Go version syntax %q", s) + } if s == "" { return } if !strings.HasPrefix(s, "go") { - return version{}, errVersionSyntax + return bad() } s = s[len("go"):] i := 0 for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { if i >= 10 || i == 0 && s[i] == '0' { - return version{}, errVersionSyntax + return bad() } v.major = 10*v.major + int(s[i]) - '0' } @@ -69,7 +69,7 @@ func parseGoVersion(s string) (v version, err error) { return } if i == 0 || s[i] != '.' { - return version{}, errVersionSyntax + return bad() } s = s[i+1:] if s == "0" { @@ -82,14 +82,15 @@ func parseGoVersion(s string) (v version, err error) { i = 0 for ; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { if i >= 10 || i == 0 && s[i] == '0' { - return version{}, errVersionSyntax + return bad() } v.minor = 10*v.minor + int(s[i]) - '0' } - if i > 0 && i == len(s) { - return - } - return version{}, errVersionSyntax + // Accept any suffix after the minor number. + // We are only looking for the language version (major.minor) + // but want to accept any valid Go version, like go1.21.0 + // and go1.21rc2. + return } // langCompat reports an error if the representation of a numeric diff --git a/src/go/types/version_test.go b/src/go/types/version_test.go new file mode 100644 index 0000000000..dc9becf9e1 --- /dev/null +++ b/src/go/types/version_test.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. + +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) + } + } +}