diff options
author | Rob Findley <rfindley@google.com> | 2021-02-11 10:23:41 -0500 |
---|---|---|
committer | Robert Findley <rfindley@google.com> | 2021-02-13 00:40:18 +0000 |
commit | b81efb7ec4348951211058cf4fdfc045c75255d6 (patch) | |
tree | 262b756be7647299688e37d153cefaa180eb570f | |
parent | a7e9b4b94804a1fbefc0c012ec510f4ee0837ffa (diff) | |
download | go-b81efb7ec4348951211058cf4fdfc045c75255d6.tar.gz go-b81efb7ec4348951211058cf4fdfc045c75255d6.zip |
[dev.regabi] go/types: add support for language version checking
This is a port of CL 289509 to go/types. It differs from that CL in
codes added to errors, to fit the new factoring of check_test.go, and to
allow go/types to import regexp in deps_test.go
For #31793
Change-Id: Ia9e4c7f5aac1493001189184227c2ebc79a76e77
Reviewed-on: https://go-review.googlesource.com/c/go/+/291317
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Griesemer <gri@golang.org>
-rw-r--r-- | src/go/build/deps_test.go | 2 | ||||
-rw-r--r-- | src/go/types/api.go | 7 | ||||
-rw-r--r-- | src/go/types/check.go | 32 | ||||
-rw-r--r-- | src/go/types/check_test.go | 36 | ||||
-rw-r--r-- | src/go/types/expr.go | 5 | ||||
-rw-r--r-- | src/go/types/stdlib_test.go | 10 | ||||
-rw-r--r-- | src/go/types/testdata/go1_12.src | 35 | ||||
-rw-r--r-- | src/go/types/version.go | 82 |
8 files changed, 186 insertions, 23 deletions
diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 02b29f498a..3fea5ecf0d 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -285,7 +285,7 @@ var depsRules = ` math/big, go/token < go/constant; - container/heap, go/constant, go/parser + container/heap, go/constant, go/parser, regexp < go/types; FMT diff --git a/src/go/types/api.go b/src/go/types/api.go index d625959817..b5bbb2d97d 100644 --- a/src/go/types/api.go +++ b/src/go/types/api.go @@ -101,6 +101,13 @@ type ImporterFrom interface { // A Config specifies the configuration for type checking. // The zero value for Config is a ready-to-use default configuration. type Config struct { + // GoVersion describes the accepted Go language version. The string + // must follow the format "go%d.%d" (e.g. "go1.12") or it must be + // empty; an empty string indicates the latest language version. + // If the format is invalid, invoking the type checker will cause a + // panic. + GoVersion string + // If IgnoreFuncBodies is set, function bodies are not // type-checked. IgnoreFuncBodies bool diff --git a/src/go/types/check.go b/src/go/types/check.go index 03798587e7..3bc8ee067c 100644 --- a/src/go/types/check.go +++ b/src/go/types/check.go @@ -8,6 +8,7 @@ package types import ( "errors" + "fmt" "go/ast" "go/constant" "go/token" @@ -84,10 +85,11 @@ type Checker struct { fset *token.FileSet pkg *Package *Info - 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 - posMap map[*Interface][]token.Pos // maps interface types to lists of embedded interface positions - pkgCnt map[string]int // counts number of imported packages with a given name (for better error messages) + version version // accepted language version + 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 + posMap map[*Interface][]token.Pos // maps interface types to lists of embedded interface positions + pkgCnt map[string]int // counts number of imported packages with a given name (for better error messages) // information collected during type-checking of a set of package files // (initialized by Files, valid only for the duration of check.Files; @@ -176,15 +178,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)) + } + return &Checker{ - conf: conf, - fset: fset, - pkg: pkg, - Info: info, - objMap: make(map[Object]*declInfo), - impMap: make(map[importKey]*Package), - posMap: make(map[*Interface][]token.Pos), - pkgCnt: make(map[string]int), + conf: conf, + fset: fset, + pkg: pkg, + Info: info, + version: version, + objMap: make(map[Object]*declInfo), + impMap: make(map[importKey]*Package), + posMap: make(map[*Interface][]token.Pos), + pkgCnt: make(map[string]int), } } diff --git a/src/go/types/check_test.go b/src/go/types/check_test.go index 7292f7bcb2..ca7d926ca9 100644 --- a/src/go/types/check_test.go +++ b/src/go/types/check_test.go @@ -47,7 +47,8 @@ import ( var ( haltOnError = flag.Bool("halt", false, "halt on error") listErrors = flag.Bool("errlist", false, "list errors") - testFiles = flag.String("files", "", "space-separated list of test files") + testFiles = flag.String("files", "", "comma-separated list of test files") + goVersion = flag.String("lang", "", "Go language version (e.g. \"go1.12\"") ) var fset = token.NewFileSet() @@ -188,7 +189,21 @@ func eliminate(t *testing.T, errmap map[string][]string, errlist []error) { } } -func checkFiles(t *testing.T, filenames []string, srcs [][]byte) { +// goVersionRx matches a Go version string using '_', e.g. "go1_12". +var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*_(0|[1-9][0-9]*)$`) + +// asGoVersion returns a regular Go language version string +// if s is a Go version string using '_' rather than '.' to +// separate the major and minor version numbers (e.g. "go1_12"). +// Otherwise it returns the empty string. +func asGoVersion(s string) string { + if goVersionRx.MatchString(s) { + return strings.Replace(s, "_", ".", 1) + } + return "" +} + +func checkFiles(t *testing.T, goVersion string, filenames []string, srcs [][]byte) { if len(filenames) == 0 { t.Fatal("no source files") } @@ -201,6 +216,11 @@ func checkFiles(t *testing.T, filenames []string, srcs [][]byte) { pkgName = files[0].Name.Name } + // if no Go version is given, consider the package name + if goVersion == "" { + goVersion = asGoVersion(pkgName) + } + if *listErrors && len(errlist) > 0 { t.Errorf("--- %s:", pkgName) for _, err := range errlist { @@ -210,6 +230,7 @@ func checkFiles(t *testing.T, filenames []string, srcs [][]byte) { // typecheck and collect typechecker errors var conf Config + conf.GoVersion = goVersion // special case for importC.src if len(filenames) == 1 { @@ -267,19 +288,20 @@ func checkFiles(t *testing.T, filenames []string, srcs [][]byte) { } // TestCheck is for manual testing of selected input files, provided with -files. +// The accepted Go language version can be controlled with the -lang flag. func TestCheck(t *testing.T) { if *testFiles == "" { return } testenv.MustHaveGoBuild(t) DefPredeclaredTestFuncs() - testPkg(t, strings.Split(*testFiles, " ")) + testPkg(t, strings.Split(*testFiles, ","), *goVersion) } func TestLongConstants(t *testing.T) { format := "package longconst\n\nconst _ = %s\nconst _ = %s // ERROR excessively long constant" src := fmt.Sprintf(format, strings.Repeat("1", 9999), strings.Repeat("1", 10001)) - checkFiles(t, []string{"longconst.go"}, [][]byte{[]byte(src)}) + checkFiles(t, "", []string{"longconst.go"}, [][]byte{[]byte(src)}) } func TestTestdata(t *testing.T) { DefPredeclaredTestFuncs(); testDir(t, "testdata") } @@ -312,12 +334,12 @@ func testDir(t *testing.T, dir string) { filenames = []string{path} } t.Run(filepath.Base(path), func(t *testing.T) { - testPkg(t, filenames) + testPkg(t, filenames, "") }) } } -func testPkg(t *testing.T, filenames []string) { +func testPkg(t *testing.T, filenames []string, goVersion string) { srcs := make([][]byte, len(filenames)) for i, filename := range filenames { src, err := os.ReadFile(filename) @@ -326,5 +348,5 @@ func testPkg(t *testing.T, filenames []string) { } srcs[i] = src } - checkFiles(t, filenames, srcs) + checkFiles(t, goVersion, filenames, srcs) } diff --git a/src/go/types/expr.go b/src/go/types/expr.go index 7f8aaed411..aec3172327 100644 --- a/src/go/types/expr.go +++ b/src/go/types/expr.go @@ -772,6 +772,10 @@ func (check *Checker) shift(x, y *operand, e ast.Expr, op token.Token) { check.invalidOp(y, _InvalidShiftCount, "shift count %s must be integer", y) x.mode = invalid return + } else if !isUnsigned(y.typ) && !check.allowVersion(check.pkg, 1, 13) { + check.invalidOp(y, _InvalidShiftCount, "signed shift count %s requires go1.13 or later", y) + x.mode = invalid + return } var yval constant.Value @@ -1152,6 +1156,7 @@ func (check *Checker) exprInternal(x *operand, e ast.Expr, hint Type) exprKind { case *ast.BasicLit: switch e.Kind { case token.INT, token.FLOAT, token.IMAG: + check.langCompat(e) // The max. mantissa precision for untyped numeric values // is 512 bits, or 4048 bits for each of the two integer // parts of a fraction for floating-point numbers that are diff --git a/src/go/types/stdlib_test.go b/src/go/types/stdlib_test.go index 71e14b85e5..979785de95 100644 --- a/src/go/types/stdlib_test.go +++ b/src/go/types/stdlib_test.go @@ -106,6 +106,7 @@ func testTestDir(t *testing.T, path string, ignore ...string) { // get per-file instructions expectErrors := false filename := filepath.Join(path, f.Name()) + goVersion := "" if comment := firstComment(filename); comment != "" { fields := strings.Fields(comment) switch fields[0] { @@ -115,13 +116,17 @@ func testTestDir(t *testing.T, path string, ignore ...string) { expectErrors = true for _, arg := range fields[1:] { if arg == "-0" || arg == "-+" || arg == "-std" { - // Marked explicitly as not expected errors (-0), + // Marked explicitly as not expecting errors (-0), // or marked as compiling runtime/stdlib, which is only done // to trigger runtime/stdlib-only error output. // In both cases, the code should typecheck. expectErrors = false break } + const prefix = "-lang=" + if strings.HasPrefix(arg, prefix) { + goVersion = arg[len(prefix):] + } } } } @@ -129,7 +134,7 @@ func testTestDir(t *testing.T, path string, ignore ...string) { // parse and type-check file file, err := parser.ParseFile(fset, filename, nil, 0) if err == nil { - conf := Config{Importer: stdLibImporter} + conf := Config{GoVersion: goVersion, Importer: stdLibImporter} _, err = conf.Check(filename, fset, []*ast.File{file}, nil) } @@ -180,7 +185,6 @@ func TestStdFixed(t *testing.T) { "issue22200b.go", // go/types does not have constraints on stack size "issue25507.go", // go/types does not have constraints on stack size "issue20780.go", // go/types does not have constraints on stack size - "issue31747.go", // go/types does not have constraints on language level (-lang=go1.12) (see #31793) "issue34329.go", // go/types does not have constraints on language level (-lang=go1.13) (see #31793) "bug251.go", // issue #34333 which was exposed with fix for #34151 "issue42058a.go", // go/types does not have constraints on channel element size diff --git a/src/go/types/testdata/go1_12.src b/src/go/types/testdata/go1_12.src new file mode 100644 index 0000000000..1e529f18be --- /dev/null +++ b/src/go/types/testdata/go1_12.src @@ -0,0 +1,35 @@ +// Copyright 2021 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. + +// Check Go language version-specific errors. + +package go1_12 // go1.12 + +// numeric literals +const ( + _ = 1_000 // ERROR "underscores in numeric literals requires go1.13 or later" + _ = 0b111 // ERROR "binary literals requires go1.13 or later" + _ = 0o567 // ERROR "0o/0O-style octal literals requires go1.13 or later" + _ = 0xabc // ok + _ = 0x0p1 // ERROR "hexadecimal floating-point literals requires go1.13 or later" + + _ = 0B111 // ERROR "binary" + _ = 0O567 // ERROR "octal" + _ = 0Xabc // ok + _ = 0X0P1 // ERROR "hexadecimal floating-point" + + _ = 1_000i // ERROR "underscores" + _ = 0b111i // ERROR "binary" + _ = 0o567i // ERROR "octal" + _ = 0xabci // ERROR "hexadecimal floating-point" + _ = 0x0p1i // ERROR "hexadecimal floating-point" +) + +// signed shift counts +var ( + s int + _ = 1 << s // ERROR "invalid operation: signed shift count s \(variable of type int\) requires go1.13 or later" + _ = 1 >> s // ERROR "signed shift count" +) + diff --git a/src/go/types/version.go b/src/go/types/version.go new file mode 100644 index 0000000000..154694169b --- /dev/null +++ b/src/go/types/version.go @@ -0,0 +1,82 @@ +// Copyright 2021 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 ( + "fmt" + "go/ast" + "go/token" + "regexp" + "strconv" + "strings" +) + +// langCompat reports an error if the representation of a numeric +// literal is not compatible with the current language version. +func (check *Checker) langCompat(lit *ast.BasicLit) { + s := lit.Value + if len(s) <= 2 || check.allowVersion(check.pkg, 1, 13) { + return + } + // len(s) > 2 + if strings.Contains(s, "_") { + check.errorf(lit, _InvalidLit, "underscores in numeric literals requires go1.13 or later") + return + } + if s[0] != '0' { + return + } + radix := s[1] + if radix == 'b' || radix == 'B' { + check.errorf(lit, _InvalidLit, "binary literals requires go1.13 or later") + return + } + if radix == 'o' || radix == 'O' { + check.errorf(lit, _InvalidLit, "0o/0O-style octal literals requires go1.13 or later") + return + } + if lit.Kind != token.INT && (radix == 'x' || radix == 'X') { + check.errorf(lit, _InvalidLit, "hexadecimal floating-point literals requires go1.13 or later") + } +} + +// allowVersion reports whether the given package +// is allowed to use version major.minor. +func (check *Checker) allowVersion(pkg *Package, major, minor int) bool { + // We assume that imported packages have all been checked, + // so we only have to check for the local package. + if pkg != check.pkg { + return true + } + ma, mi := check.version.major, check.version.minor + return ma == 0 && mi == 0 || ma > major || ma == major && mi >= minor +} + +type version struct { + major, minor int +} + +// 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) { + if s == "" { + return + } + matches := goVersionRx.FindStringSubmatch(s) + if matches == nil { + err = fmt.Errorf(`should be something like "go1.12"`) + return + } + v.major, err = strconv.Atoi(matches[1]) + if err != nil { + return + } + v.minor, err = strconv.Atoi(matches[2]) + return +} + +// goVersionRx matches a Go version string, e.g. "go1.12". +var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) |