Skip to content

Commit bc9aaa8

Browse files
committed
Overhaul version handling
Go 1.21 changed the meaning of the "go" statement in go.mod files to enforce a minimum toolchain version, as opposed to a maximum language version. This means we no longer need the '-go' CLI flag to be able to specify the minimum targeted Go version. Furthermore, go/types.Config gained a GoVersion field and its value gets propagated to go/types.Package.GoVersion, which means that we no longer need per-analyzer "go" flags to access the targeted Go version. Go 1.22 expanded go/types to report per-file versions (as //go:build lines in individual files can downgrade and upgrade to different Go versions), which means that we no longer need to parse these lines manually. Go 1.22 also added the go/version package to allow comparing string-based Go versions. This means we no longer need to parse strings to turn them into integers. We update our tests to be grouped by Go versions, to ensure that checks work with the intended minimum versions of Go and to make it easier to test version-specific behavior. Each tested Go version for a check is treated as its own module. To avoid checking in actual go.mod files, we use go/packages.Config.Overlay to synthesize them. This requires updating to the latest commit of x/tools which includes a fix for synthesizing go.mod files. Closes: gh-105 Closes: gh-1464 Closes: gh-1463
1 parent 80d98d7 commit bc9aaa8

File tree

378 files changed

+562
-729
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

378 files changed

+562
-729
lines changed

analysis/code/code.go

Lines changed: 44 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,19 @@
22
package code
33

44
import (
5-
"flag"
65
"fmt"
76
"go/ast"
87
"go/build/constraint"
98
"go/constant"
109
"go/token"
1110
"go/types"
11+
"go/version"
1212
"path/filepath"
13-
"strconv"
1413
"strings"
1514

1615
"honnef.co/go/tools/analysis/facts/generated"
1716
"honnef.co/go/tools/analysis/facts/purity"
1817
"honnef.co/go/tools/analysis/facts/tokenfile"
19-
"honnef.co/go/tools/analysis/lint"
2018
"honnef.co/go/tools/go/ast/astutil"
2119
"honnef.co/go/tools/go/types/typeutil"
2220
"honnef.co/go/tools/knowledge"
@@ -35,7 +33,7 @@ func IsOfStringConvertibleByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
3533
return false
3634
}
3735
elem := types.Unalias(typ.Elem())
38-
if LanguageVersion(pass, expr) >= 18 {
36+
if version.Compare(LanguageVersion(pass, expr), "go1.18") >= 0 {
3937
// Before Go 1.18, one could not directly convert from []T (where 'type T byte')
4038
// to string. See also https://github.com/golang/go/issues/23536.
4139
elem = elem.Underlying()
@@ -436,7 +434,9 @@ func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result
436434
}
437435
}
438436

439-
func LanguageVersion(pass *analysis.Pass, node Positioner) int {
437+
// LanguageVersion returns the version of the Go language that node has access to. This
438+
// might differ from the version of the Go standard library.
439+
func LanguageVersion(pass *analysis.Pass, node Positioner) string {
440440
// As of Go 1.21, two places can specify the minimum Go version:
441441
// - 'go' directives in go.mod and go.work files
442442
// - individual files by using '//go:build'
@@ -478,89 +478,64 @@ func LanguageVersion(pass *analysis.Pass, node Positioner) int {
478478
// relevant language changes before Go 1.22 will lead to type-checking failures and never reach
479479
// us.
480480
//
481-
// It is not clear if per-file upgrading is possible in GOPATH mode. This needs clarification.
481+
// Per-file upgrading is permitted in GOPATH mode.
482482

483-
f := File(pass, node)
484-
var n int
485-
if v := f.GoVersion; v != "" {
486-
var ok bool
487-
n, ok = lint.ParseGoVersion(v)
488-
if !ok {
489-
panic(fmt.Sprintf("unexpected failure parsing version %q", v))
490-
}
491-
} else if v := pass.Pkg.GoVersion(); v != "" {
492-
var ok bool
493-
n, ok = lint.ParseGoVersion(v)
494-
if !ok {
495-
panic(fmt.Sprintf("unexpected failure parsing version %q", v))
496-
}
497-
} else {
498-
v, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter)
499-
if !ok {
500-
panic("requested Go version, but analyzer has no version flag")
501-
}
502-
n = v.Get().(int)
503-
}
504-
505-
return n
483+
// If the file has its own Go version, we will return that. Otherwise, we default to
484+
// the type checker's GoVersion, which is populated from either the Go module, or from
485+
// our '-go' flag.
486+
return pass.TypesInfo.FileVersions[File(pass, node)]
506487
}
507488

508-
func StdlibVersion(pass *analysis.Pass, node Positioner) int {
509-
var n int
510-
if v := pass.Pkg.GoVersion(); v != "" {
511-
var ok bool
512-
n, ok = lint.ParseGoVersion(v)
513-
if !ok {
514-
panic(fmt.Sprintf("unexpected failure parsing version %q", v))
515-
}
516-
} else {
517-
v, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter)
518-
if !ok {
519-
panic("requested Go version, but analyzer has no version flag")
520-
}
521-
n = v.Get().(int)
522-
}
489+
// StdlibVersion returns the version of the Go standard library that node can expect to
490+
// have access to. This might differ from the language version for versions of Go older
491+
// than 1.21.
492+
func StdlibVersion(pass *analysis.Pass, node Positioner) string {
493+
// The Go version as specified in go.mod or via the '-go' flag
494+
n := pass.Pkg.GoVersion()
523495

524496
f := File(pass, node)
525497
if f == nil {
526498
panic(fmt.Sprintf("no file found for node with position %s", pass.Fset.PositionFor(node.Pos(), false)))
527499
}
528500

529-
if v := f.GoVersion; v != "" {
530-
nf, err := strconv.Atoi(strings.TrimPrefix(v, "go1."))
531-
if err != nil {
532-
panic(fmt.Sprintf("unexpected error: %s", err))
533-
}
534-
535-
if n < 21 {
536-
// Before Go 1.21, the Go version set in go.mod specified the maximum language version
537-
// available to the module. It wasn't uncommon to set the version to Go 1.20 but only
538-
// use 1.20 functionality (both language and stdlib) in files tagged for 1.20, and
539-
// supporting a lower version overall. As such, a file tagged lower than the module
540-
// version couldn't expect to have access to the standard library of the version set in
541-
// go.mod.
501+
if nf := f.GoVersion; nf != "" {
502+
if version.Compare(n, "go1.21") == -1 {
503+
// Before Go 1.21, the Go version set in go.mod specified the maximum language
504+
// version available to the module. It wasn't uncommon to set the version to
505+
// Go 1.20 but restrict usage of 1.20 functionality (both language and stdlib)
506+
// to files tagged for 1.20, and supporting a lower version overall. As such,
507+
// a file tagged lower than the module version couldn't expect to have access
508+
// to the standard library of the version set in go.mod.
509+
//
510+
// At the same time, a file tagged higher than the module version, while not
511+
// able to use newer language features, would still have been able to use a
512+
// newer standard library.
542513
//
543514
// While Go 1.21's behavior has been backported to 1.19.11 and 1.20.6, users'
544515
// expectations have not.
545-
n = nf
516+
return nf
546517
} else {
547-
// Go 1.21 and newer refuse to build modules that depend on versions newer than the Go
548-
// version. This means that in a 1.22 module with a file tagged as 1.17, the file can
549-
// expect to have access to 1.22's standard library.
518+
// Go 1.21 and newer refuse to build modules that depend on versions newer
519+
// than the used version of the Go toolchain. This means that in a 1.22 module
520+
// with a file tagged as 1.17, the file can expect to have access to 1.22's
521+
// standard library (but not to 1.22 language features). A file tagged with a
522+
// version higher than the minimum version has access to the newer standard
523+
// library (and language features.)
550524
//
551-
// Do note that strictly speaking we're conflating the Go version and the module version in
552-
// our check. Nothing is stopping a user from using Go 1.17 to build a Go 1.22 module, in
553-
// which case the 1.17 file will not have acces to the 1.22 standard library. However, we
554-
// believe that if a module requires 1.21 or newer, then the author clearly expects the new
555-
// behavior, and doesn't care for the old one. Otherwise they would've specified an older
556-
// version.
525+
// Do note that strictly speaking we're conflating the Go version and the
526+
// module version in our check. Nothing is stopping a user from using Go 1.17
527+
// (which didn't implement the new rules for versions in go.mod) to build a Go
528+
// 1.22 module, in which case a file tagged with go1.17 will not have acces to the 1.22
529+
// standard library. However, we believe that if a module requires 1.21 or
530+
// newer, then the author clearly expects the new behavior, and doesn't care
531+
// for the old one. Otherwise they would've specified an older version.
557532
//
558533
// In other words, the module version also specifies what it itself actually means, with
559534
// >=1.21 being a minimum version for the toolchain, and <1.21 being a maximum version for
560535
// the language.
561536

562-
if nf > n {
563-
n = nf
537+
if version.Compare(nf, n) == 1 {
538+
return nf
564539
}
565540
}
566541
}

analysis/lint/lint.go

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@
33
package lint
44

55
import (
6-
"flag"
76
"fmt"
87
"go/ast"
9-
"go/build"
108
"go/token"
11-
"regexp"
12-
"strconv"
139
"strings"
1410

1511
"golang.org/x/tools/go/analysis"
@@ -27,22 +23,12 @@ type Analyzer struct {
2723

2824
func (a *Analyzer) initialize() {
2925
a.Analyzer.Doc = a.Doc.String()
30-
if a.Analyzer.Flags.Usage == nil {
31-
fs := flag.NewFlagSet("", flag.PanicOnError)
32-
fs.Var(newVersionFlag(), "go", "Target Go version")
33-
a.Analyzer.Flags = *fs
34-
}
3526
a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
3627
}
3728

3829
func InitializeAnalyzer(a *Analyzer) *Analyzer {
3930
a.Analyzer.Doc = a.Doc.String()
4031
a.Analyzer.URL = "https://staticcheck.dev/docs/checks/#" + a.Analyzer.Name
41-
if a.Analyzer.Flags.Usage == nil {
42-
fs := flag.NewFlagSet("", flag.PanicOnError)
43-
fs.Var(newVersionFlag(), "go", "Target Go version")
44-
a.Analyzer.Flags = *fs
45-
}
4632
a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
4733
return a
4834
}
@@ -206,51 +192,6 @@ func (doc *Documentation) String() string {
206192
return doc.Format(true)
207193
}
208194

209-
func newVersionFlag() flag.Getter {
210-
tags := build.Default.ReleaseTags
211-
v := tags[len(tags)-1][2:]
212-
version := new(VersionFlag)
213-
if err := version.Set(v); err != nil {
214-
panic(fmt.Sprintf("internal error: %s", err))
215-
}
216-
return version
217-
}
218-
219-
type VersionFlag int
220-
221-
func (v *VersionFlag) String() string {
222-
return fmt.Sprintf("1.%d", *v)
223-
}
224-
225-
var goVersionRE = regexp.MustCompile(`^(?:go)?1.(\d+).*$`)
226-
227-
// ParseGoVersion parses Go versions of the form 1.M, 1.M.N, or 1.M.NrcR, with an optional "go" prefix. It assumes that
228-
// versions have already been verified and are valid.
229-
func ParseGoVersion(s string) (int, bool) {
230-
m := goVersionRE.FindStringSubmatch(s)
231-
if m == nil {
232-
return 0, false
233-
}
234-
n, err := strconv.Atoi(m[1])
235-
if err != nil {
236-
return 0, false
237-
}
238-
return n, true
239-
}
240-
241-
func (v *VersionFlag) Set(s string) error {
242-
n, ok := ParseGoVersion(s)
243-
if !ok {
244-
return fmt.Errorf("invalid Go version: %q", s)
245-
}
246-
*v = VersionFlag(n)
247-
return nil
248-
}
249-
250-
func (v *VersionFlag) Get() interface{} {
251-
return int(*v)
252-
}
253-
254195
// ExhaustiveTypeSwitch panics when called. It can be used to ensure
255196
// that type switches are exhaustive.
256197
func ExhaustiveTypeSwitch(v interface{}) {

0 commit comments

Comments
 (0)