Skip to content

Commit

Permalink
go/types: record Config.GoVersion for reporting in Package.GoVersion …
Browse files Browse the repository at this point in the history
…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>
  • Loading branch information
rsc committed Jul 6, 2023
1 parent 36ea4f9 commit b490bdc
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 55 deletions.
1 change: 1 addition & 0 deletions api/go1.21.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 14 additions & 7 deletions src/cmd/compile/internal/types2/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/compile/internal/types2/sizeof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestSizeof(t *testing.T) {

// Misc
{Scope{}, 60, 104},
{Package{}, 36, 72},
{Package{}, 44, 88},
{_TypeSet{}, 28, 56},
}

Expand Down
23 changes: 12 additions & 11 deletions src/cmd/compile/internal/types2/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package types2

import (
"cmd/compile/internal/syntax"
"errors"
"fmt"
"strings"
)
Expand Down Expand Up @@ -44,31 +43,32 @@ 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'
}
if i > 0 && i == len(s) {
return
}
if i == 0 || s[i] != '.' {
return version{}, errVersionSyntax
return bad()
}
s = s[i+1:]
if s == "0" {
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 25 additions & 16 deletions src/go/types/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"go/ast"
"go/constant"
"go/token"
"internal/goversion"
. "internal/types/errors"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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
}
Expand Down
21 changes: 14 additions & 7 deletions src/go/types/package.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/go/types/sizeof_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 12 additions & 11 deletions src/go/types/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package types

import (
"errors"
"fmt"
"go/ast"
"go/token"
Expand Down Expand Up @@ -45,31 +44,32 @@ 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'
}
if i > 0 && i == len(s) {
return
}
if i == 0 || s[i] != '.' {
return version{}, errVersionSyntax
return bad()
}
s = s[i+1:]
if s == "0" {
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/go/types/version_test.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.

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)
}
}
}

0 comments on commit b490bdc

Please sign in to comment.