go/analysis/passes/modernize: respect bootstrap toolchain version fileUsesGoVersion (formerly fileUses) now checks not only the FileVersion but the bootstrap toolchain version too, so that, for example, we don't prematurely modernize packages in the compiler to use features of go1.26. Unfortunately this change cannot be tested via the analysistest framework because it would require that we overwrite the sources of actual std packages. lostcancel and maprange now both use copies of fileUsesGoVersion too. I will merge them into analyzerutil once CL 718081 is merged. For golang/go#71859 Change-Id: I4b8ede616a46c03bc58f017aa7fc522d1df535f7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/718503 Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 8682263..fa764e7 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -14,6 +14,8 @@ "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" + "golang.org/x/tools/internal/packagepath" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -55,8 +57,8 @@ switch n := n.(type) { case *ast.File: // Only traverse the file if its goversion is strictly before go1.22. - goversion := versions.FileVersion(pass.TypesInfo, n) - return versions.Before(goversion, versions.Go1_22) + return !fileUsesVersion(pass, n, versions.Go1_22) + case *ast.RangeStmt: body = n.Body addVar(n.Key) @@ -356,6 +358,7 @@ } // Check that we are calling a method <method> + // TODO(adonovan): use [typesinternal.IsMethodNamed]. f := typeutil.StaticCallee(info, call) if f == nil || f.Name() != method { return false @@ -370,3 +373,30 @@ _, named := typesinternal.ReceiverNamed(recv) return typesinternal.IsTypeNamed(named, pkgPath, typeName) } + +// fileUsesVersion reports whether the specified file may use +// features of the specified version of Go (e.g. "go1.24"). +// +// Tip: we recommend using this check "late", just before calling +// pass.Report, rather than "early" (when entering each ast.File, or +// each candidate node of interest, during the traversal), because the +// operation is not free, yet is not a highly selective filter: the +// fraction of files that pass most version checks is high and +// increases over time. +// +// TODO(adonovan): move to analyzer library. +func fileUsesVersion(pass *analysis.Pass, file *ast.File, version string) bool { + // Standard packages that are part of toolchain bootstrapping + // are not considered to use a version of Go later than the + // current bootstrap toolchain version. + pkgpath := pass.Pkg.Path() + if packagepath.IsStdPackage(pkgpath) && + stdlib.IsBootstrapPackage(pkgpath) && + versions.Before(version, stdlib.BootstrapVersion.String()) { + return false // package must bootstrap + } + if versions.Before(pass.TypesInfo.FileVersions[file], version) { + return false // file version is too old + } + return true // ok +}
diff --git a/go/analysis/passes/modernize/any.go b/go/analysis/passes/modernize/any.go index 05999f8..3685f57 100644 --- a/go/analysis/passes/modernize/any.go +++ b/go/analysis/passes/modernize/any.go
@@ -9,9 +9,9 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" + "golang.org/x/tools/internal/versions" ) var AnyAnalyzer = &analysis.Analyzer{ @@ -29,9 +29,7 @@ func runAny(pass *analysis.Pass) (any, error) { skipGenerated(pass) - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - - for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.18") { + for curFile := range filesUsing(pass, versions.Go1_18) { for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) { iface := curIface.Node().(*ast.InterfaceType)
diff --git a/go/analysis/passes/modernize/bloop.go b/go/analysis/passes/modernize/bloop.go index 90b23ad..70cfdfe 100644 --- a/go/analysis/passes/modernize/bloop.go +++ b/go/analysis/passes/modernize/bloop.go
@@ -22,6 +22,7 @@ "golang.org/x/tools/internal/moreiters" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var BLoopAnalyzer = &analysis.Analyzer{ @@ -52,9 +53,8 @@ } var ( - inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) - info = pass.TypesInfo + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo ) // edits computes the text edits for a matched for/range loop @@ -102,7 +102,7 @@ (*ast.ForStmt)(nil), (*ast.RangeStmt)(nil), } - for curFile := range filesUsing(inspect, info, "go1.24") { + for curFile := range filesUsing(pass, versions.Go1_24) { for curLoop := range curFile.Preorder(loops...) { switch n := curLoop.Node().(type) { case *ast.ForStmt:
diff --git a/go/analysis/passes/modernize/errorsastype.go b/go/analysis/passes/modernize/errorsastype.go index b6387ad..b3696e2 100644 --- a/go/analysis/passes/modernize/errorsastype.go +++ b/go/analysis/passes/modernize/errorsastype.go
@@ -22,6 +22,7 @@ "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var errorsastypeAnalyzer = &analysis.Analyzer{ @@ -97,7 +98,7 @@ } file := astutil.EnclosingFile(curDeclStmt) - if !fileUses(info, file, "go1.26") { + if !fileUsesVersion(pass, file, versions.Go1_26) { continue // errors.AsType is too new }
diff --git a/go/analysis/passes/modernize/fmtappendf.go b/go/analysis/passes/modernize/fmtappendf.go index f2e5360..afc9440 100644 --- a/go/analysis/passes/modernize/fmtappendf.go +++ b/go/analysis/passes/modernize/fmtappendf.go
@@ -18,6 +18,7 @@ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var FmtAppendfAnalyzer = &analysis.Analyzer{ @@ -50,7 +51,7 @@ conv := curCall.Parent().Node().(*ast.CallExpr) tv := pass.TypesInfo.Types[conv.Fun] if tv.IsType() && types.Identical(tv.Type, byteSliceType) && - fileUses(pass.TypesInfo, astutil.EnclosingFile(curCall), "go1.19") { + fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) { // Have: []byte(fmt.SprintX(...)) // Find "Sprint" identifier.
diff --git a/go/analysis/passes/modernize/forvar.go b/go/analysis/passes/modernize/forvar.go index 76e3a8a..20ac3e8 100644 --- a/go/analysis/passes/modernize/forvar.go +++ b/go/analysis/passes/modernize/forvar.go
@@ -10,11 +10,11 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/versions" ) var ForVarAnalyzer = &analysis.Analyzer{ @@ -45,8 +45,7 @@ func forvar(pass *analysis.Pass) (any, error) { skipGenerated(pass) - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.22") { + for curFile := range filesUsing(pass, versions.Go1_22) { for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) { loop := curLoop.Node().(*ast.RangeStmt) if loop.Tok != token.DEFINE {
diff --git a/go/analysis/passes/modernize/maps.go b/go/analysis/passes/modernize/maps.go index 3072cf6..7ef2a21 100644 --- a/go/analysis/passes/modernize/maps.go +++ b/go/analysis/passes/modernize/maps.go
@@ -21,6 +21,7 @@ "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" ) var MapsLoopAnalyzer = &analysis.Analyzer{ @@ -223,8 +224,7 @@ } // Find all range loops around m[k] = v. - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.23") { + for curFile := range filesUsing(pass, versions.Go1_23) { file := curFile.Node().(*ast.File) for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
diff --git a/go/analysis/passes/modernize/minmax.go b/go/analysis/passes/modernize/minmax.go index 7ebf837..2877e93 100644 --- a/go/analysis/passes/modernize/minmax.go +++ b/go/analysis/passes/modernize/minmax.go
@@ -21,6 +21,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var MinMaxAnalyzer = &analysis.Analyzer{ @@ -201,8 +202,7 @@ // Find all "if a < b { lhs = rhs }" statements. info := pass.TypesInfo - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for curFile := range filesUsing(inspect, info, "go1.21") { + for curFile := range filesUsing(pass, versions.Go1_21) { astFile := curFile.Node().(*ast.File) for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { ifStmt := curIfStmt.Node().(*ast.IfStmt)
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go index 28bd101..27c07ac 100644 --- a/go/analysis/passes/modernize/modernize.go +++ b/go/analysis/passes/modernize/modernize.go
@@ -16,6 +16,7 @@ "strings" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal/generated" @@ -95,21 +96,25 @@ // filesUsing returns a cursor for each *ast.File in the inspector // that uses at least the specified version of Go (e.g. "go1.24"). // +// The pass's analyzer must require [inspect.Analyzer]. +// // TODO(adonovan): opt: eliminate this function, instead following the -// approach of [fmtappendf], which uses typeindex and [fileUses]. -// See "Tip" at [fileUses] for motivation. -func filesUsing(inspect *inspector.Inspector, info *types.Info, version string) iter.Seq[inspector.Cursor] { +// approach of [fmtappendf], which uses typeindex and [fileUsesGoVersion]. +// See "Tip" at [fileUsesGoVersion] for motivation. +func filesUsing(pass *analysis.Pass, version string) iter.Seq[inspector.Cursor] { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + return func(yield func(inspector.Cursor) bool) { for curFile := range inspect.Root().Children() { file := curFile.Node().(*ast.File) - if !versions.Before(info.FileVersions[file], version) && !yield(curFile) { + if fileUsesVersion(pass, file, version) && !yield(curFile) { break } } } } -// fileUses reports whether the specified file uses at least the +// fileUsesVersion reports whether the specified file may use features of the // specified version of Go (e.g. "go1.24"). // // Tip: we recommend using this check "late", just before calling @@ -118,8 +123,22 @@ // operation is not free, yet is not a highly selective filter: the // fraction of files that pass most version checks is high and // increases over time. -func fileUses(info *types.Info, file *ast.File, version string) bool { - return !versions.Before(info.FileVersions[file], version) +// +// TODO(adonovan): move to analyzer library. +func fileUsesVersion(pass *analysis.Pass, file *ast.File, version string) bool { + // Standard packages that are part of toolchain bootstrapping + // are not considered to use a version of Go later than the + // current bootstrap toolchain version. + pkgpath := pass.Pkg.Path() + if packagepath.IsStdPackage(pkgpath) && + stdlib.IsBootstrapPackage(pkgpath) && + versions.Before(version, stdlib.BootstrapVersion.String()) { + return false // package must bootstrap + } + if versions.Before(pass.TypesInfo.FileVersions[file], version) { + return false // file version is too old + } + return true // ok } // within reports whether the current pass is analyzing one of the
diff --git a/go/analysis/passes/modernize/newexpr.go b/go/analysis/passes/modernize/newexpr.go index 2d82e6f..3a3f5a5 100644 --- a/go/analysis/passes/modernize/newexpr.go +++ b/go/analysis/passes/modernize/newexpr.go
@@ -19,6 +19,7 @@ "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/versions" ) var NewExprAnalyzer = &analysis.Analyzer{ @@ -60,7 +61,7 @@ // Check file version. file := astutil.EnclosingFile(curFuncDecl) - if !fileUses(info, file, "go1.26") { + if !fileUsesVersion(pass, file, versions.Go1_26) { continue // new(expr) not available in this file } @@ -133,7 +134,7 @@ // Check file version. file := astutil.EnclosingFile(curCall) - if !fileUses(info, file, "go1.26") { + if !fileUsesVersion(pass, file, versions.Go1_26) { continue // new(expr) not available in this file }
diff --git a/go/analysis/passes/modernize/omitzero.go b/go/analysis/passes/modernize/omitzero.go index 1405d13..34e2ed7 100644 --- a/go/analysis/passes/modernize/omitzero.go +++ b/go/analysis/passes/modernize/omitzero.go
@@ -12,10 +12,10 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/versions" ) var OmitZeroAnalyzer = &analysis.Analyzer{ @@ -101,12 +101,10 @@ func omitzero(pass *analysis.Pass) (any, error) { skipGenerated(pass) - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - info := pass.TypesInfo - for curFile := range filesUsing(inspect, info, "go1.24") { + for curFile := range filesUsing(pass, versions.Go1_24) { for curStruct := range curFile.Preorder((*ast.StructType)(nil)) { for _, curField := range curStruct.Node().(*ast.StructType).Fields.List { - checkOmitEmptyField(pass, info, curField) + checkOmitEmptyField(pass, pass.TypesInfo, curField) } } }
diff --git a/go/analysis/passes/modernize/plusbuild.go b/go/analysis/passes/modernize/plusbuild.go index e8af807..e5e2fdd 100644 --- a/go/analysis/passes/modernize/plusbuild.go +++ b/go/analysis/passes/modernize/plusbuild.go
@@ -12,6 +12,7 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/goplsexport" + "golang.org/x/tools/internal/versions" ) var plusBuildAnalyzer = &analysis.Analyzer{ @@ -28,7 +29,7 @@ func plusbuild(pass *analysis.Pass) (any, error) { check := func(f *ast.File) { - if !fileUses(pass.TypesInfo, f, "go1.18") { + if !fileUsesVersion(pass, f, versions.Go1_18) { return }
diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go index eebe740..d887a89 100644 --- a/go/analysis/passes/modernize/rangeint.go +++ b/go/analysis/passes/modernize/rangeint.go
@@ -21,6 +21,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var RangeIntAnalyzer = &analysis.Analyzer{ @@ -68,12 +69,12 @@ func rangeint(pass *analysis.Pass) (any, error) { skipGenerated(pass) - info := pass.TypesInfo + var ( + info = pass.TypesInfo + typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + ) - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - typeindex := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) - - for curFile := range filesUsing(inspect, info, "go1.22") { + for curFile := range filesUsing(pass, versions.Go1_22) { nextLoop: for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) { loop := curLoop.Node().(*ast.ForStmt)
diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go index c9b0fa4..6f10d2a 100644 --- a/go/analysis/passes/modernize/reflect.go +++ b/go/analysis/passes/modernize/reflect.go
@@ -89,7 +89,7 @@ } file := astutil.EnclosingFile(curCall) - if versions.Before(info.FileVersions[file], "go1.22") { + if !fileUsesVersion(pass, file, versions.Go1_22) { continue // TypeFor requires go1.22 } tokFile := pass.Fset.File(file.Pos())
diff --git a/go/analysis/passes/modernize/slices.go b/go/analysis/passes/modernize/slices.go index 23b3952..f527d5d 100644 --- a/go/analysis/passes/modernize/slices.go +++ b/go/analysis/passes/modernize/slices.go
@@ -13,13 +13,13 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" ) // Warning: this analyzer is not safe to enable by default. @@ -205,8 +205,7 @@ skip := make(map[*ast.CallExpr]bool) // Visit calls of form append(x, y...). - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - for curFile := range filesUsing(inspect, info, "go1.21") { + for curFile := range filesUsing(pass, versions.Go1_21) { file := curFile.Node().(*ast.File) for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
diff --git a/go/analysis/passes/modernize/slicescontains.go b/go/analysis/passes/modernize/slicescontains.go index 87aa4f5..35d3c5d 100644 --- a/go/analysis/passes/modernize/slicescontains.go +++ b/go/analysis/passes/modernize/slicescontains.go
@@ -21,6 +21,7 @@ "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var SlicesContainsAnalyzer = &analysis.Analyzer{ @@ -75,9 +76,8 @@ } var ( - inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) - info = pass.TypesInfo + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo ) // check is called for each RangeStmt of this form: @@ -386,7 +386,7 @@ } } - for curFile := range filesUsing(inspect, info, "go1.21") { + for curFile := range filesUsing(pass, versions.Go1_21) { file := curFile.Node().(*ast.File) for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
diff --git a/go/analysis/passes/modernize/slicesdelete.go b/go/analysis/passes/modernize/slicesdelete.go index b3e063d..ff38e2f 100644 --- a/go/analysis/passes/modernize/slicesdelete.go +++ b/go/analysis/passes/modernize/slicesdelete.go
@@ -12,12 +12,12 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" ) // Warning: this analyzer is not safe to enable by default (not nil-preserving). @@ -45,7 +45,6 @@ return nil, nil } - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) info := pass.TypesInfo report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) { insert := func(pos token.Pos, text string) analysis.TextEdit { @@ -55,7 +54,7 @@ return types.Identical(types.Default(info.TypeOf(e)), builtinInt.Type()) } isIntShadowed := func() bool { - scope := pass.TypesInfo.Scopes[file].Innermost(call.Lparen) + scope := info.Scopes[file].Innermost(call.Lparen) if _, obj := scope.LookupParent("int", call.Lparen); obj != builtinInt { return true // int type is shadowed } @@ -130,7 +129,7 @@ }}, }) } - for curFile := range filesUsing(inspect, info, "go1.21") { + for curFile := range filesUsing(pass, versions.Go1_21) { file := curFile.Node().(*ast.File) for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { call := curCall.Node().(*ast.CallExpr)
diff --git a/go/analysis/passes/modernize/sortslice.go b/go/analysis/passes/modernize/sortslice.go index 66af16d..1dd2c5e 100644 --- a/go/analysis/passes/modernize/sortslice.go +++ b/go/analysis/passes/modernize/sortslice.go
@@ -17,6 +17,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) // (Not to be confused with go/analysis/passes/sortslice.) @@ -87,7 +88,7 @@ } file := astutil.EnclosingFile(curCall) if isIndex(compare.X, i) && isIndex(compare.Y, j) && - fileUses(info, file, "go1.21") { + fileUsesVersion(pass, file, versions.Go1_21) { // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) prefix, importEdits := refactor.AddImport(
diff --git a/go/analysis/passes/modernize/stditerators.go b/go/analysis/passes/modernize/stditerators.go index 0621448..8f2ce70 100644 --- a/go/analysis/passes/modernize/stditerators.go +++ b/go/analysis/passes/modernize/stditerators.go
@@ -313,7 +313,7 @@ // may be somewhat expensive.) if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok { panic("no version found") - } else if file := astutil.EnclosingFile(curLenCall); !fileUses(info, file, v.String()) { + } else if !fileUsesVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) { continue nextCall }
diff --git a/go/analysis/passes/modernize/stringscut.go b/go/analysis/passes/modernize/stringscut.go index 598920d..0cc575d 100644 --- a/go/analysis/passes/modernize/stringscut.go +++ b/go/analysis/passes/modernize/stringscut.go
@@ -26,6 +26,7 @@ "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var stringscutAnalyzer = &analysis.Analyzer{ @@ -131,8 +132,7 @@ nextcall: for curCall := range index.Calls(obj) { // Check file version. - file := astutil.EnclosingFile(curCall) - if !fileUses(info, file, "go1.18") { + if !fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) { continue // strings.Index not available in this file } indexCall := curCall.Node().(*ast.CallExpr) // the call to strings.Index, etc.
diff --git a/go/analysis/passes/modernize/stringscutprefix.go b/go/analysis/passes/modernize/stringscutprefix.go index 9e76f95..113b76c 100644 --- a/go/analysis/passes/modernize/stringscutprefix.go +++ b/go/analysis/passes/modernize/stringscutprefix.go
@@ -12,7 +12,6 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" @@ -21,6 +20,7 @@ "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var StringsCutPrefixAnalyzer = &analysis.Analyzer{ @@ -59,9 +59,8 @@ skipGenerated(pass) var ( - inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) - info = pass.TypesInfo + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo stringsTrimPrefix = index.Object("strings", "TrimPrefix") bytesTrimPrefix = index.Object("bytes", "TrimPrefix") @@ -72,7 +71,7 @@ return nil, nil } - for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.20") { + for curFile := range filesUsing(pass, versions.Go1_20) { for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { ifStmt := curIfStmt.Node().(*ast.IfStmt)
diff --git a/go/analysis/passes/modernize/stringsseq.go b/go/analysis/passes/modernize/stringsseq.go index ef2b546..9bacddb 100644 --- a/go/analysis/passes/modernize/stringsseq.go +++ b/go/analysis/passes/modernize/stringsseq.go
@@ -13,12 +13,12 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/edge" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var StringsSeqAnalyzer = &analysis.Analyzer{ @@ -51,9 +51,8 @@ skipGenerated(pass) var ( - inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) - info = pass.TypesInfo + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo stringsSplit = index.Object("strings", "Split") stringsFields = index.Object("strings", "Fields") @@ -64,7 +63,7 @@ return nil, nil } - for curFile := range filesUsing(inspect, info, "go1.24") { + for curFile := range filesUsing(pass, versions.Go1_24) { for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { rng := curRange.Node().(*ast.RangeStmt)
diff --git a/go/analysis/passes/modernize/testingcontext.go b/go/analysis/passes/modernize/testingcontext.go index 558cf14..63a7433 100644 --- a/go/analysis/passes/modernize/testingcontext.go +++ b/go/analysis/passes/modernize/testingcontext.go
@@ -23,6 +23,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var TestingContextAnalyzer = &analysis.Analyzer{ @@ -137,7 +138,7 @@ testObj = isTestFn(info, n) } } - if testObj != nil && fileUses(info, astutil.EnclosingFile(cur), "go1.24") { + if testObj != nil && fileUsesVersion(pass, astutil.EnclosingFile(cur), versions.Go1_24) { // Have a test function. Check that we can resolve the relevant // testing.{T,B,F} at the current position. if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj {
diff --git a/go/analysis/passes/modernize/waitgroup.go b/go/analysis/passes/modernize/waitgroup.go index b890f33..79c6939 100644 --- a/go/analysis/passes/modernize/waitgroup.go +++ b/go/analysis/passes/modernize/waitgroup.go
@@ -20,6 +20,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" ) var WaitGroupAnalyzer = &analysis.Analyzer{ @@ -128,7 +129,7 @@ } file := astutil.EnclosingFile(curAddCall) - if !fileUses(info, file, "go1.25") { + if !fileUsesVersion(pass, file, versions.Go1_25) { continue } tokFile := pass.Fset.File(file.Pos())
diff --git a/gopls/internal/analysis/maprange/maprange.go b/gopls/internal/analysis/maprange/maprange.go index 1e44a4b..1b64e24 100644 --- a/gopls/internal/analysis/maprange/maprange.go +++ b/gopls/internal/analysis/maprange/maprange.go
@@ -13,10 +13,11 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/cursorutil" "golang.org/x/tools/internal/analysisinternal" typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex" "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/packagepath" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/versions" ) @@ -90,9 +91,9 @@ if pkg == xmaps && isSet(rangeStmt.Key) && rangeStmt.Value == nil { // If we have: for i := range maps.Keys(m) (using x/exp/maps), // Replace with: for i := range len(m) + // (This requires Go 1.22.) replace = "len" - canRangeOverInt := fileUses(pass.TypesInfo, curCall, "go1.22") - if !canRangeOverInt { + if !fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_22) { pass.Report(analysis.Diagnostic{ Pos: call.Pos(), End: call.End(), @@ -150,9 +151,29 @@ return expr != nil && (!ok || ident.Name != "_") } -// fileUses reports whether the file containing the specified cursor -// uses at least the specified version of Go (e.g. "go1.24"). -func fileUses(info *types.Info, c inspector.Cursor, version string) bool { - file, _ := cursorutil.FirstEnclosing[*ast.File](c) - return !versions.Before(info.FileVersions[file], version) +// fileUsesVersion reports whether the specified file may use +// features of the specified version of Go (e.g. "go1.24"). +// +// Tip: we recommend using this check "late", just before calling +// pass.Report, rather than "early" (when entering each ast.File, or +// each candidate node of interest, during the traversal), because the +// operation is not free, yet is not a highly selective filter: the +// fraction of files that pass most version checks is high and +// increases over time. +// +// TODO(adonovan): move to analyzer library. +func fileUsesVersion(pass *analysis.Pass, file *ast.File, version string) bool { + // Standard packages that are part of toolchain bootstrapping + // are not considered to use a version of Go later than the + // current bootstrap toolchain version. + pkgpath := pass.Pkg.Path() + if packagepath.IsStdPackage(pkgpath) && + stdlib.IsBootstrapPackage(pkgpath) && + versions.Before(version, stdlib.BootstrapVersion.String()) { + return false // package must bootstrap + } + if versions.Before(pass.TypesInfo.FileVersions[file], version) { + return false // file version is too old + } + return true // ok }
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index fae6d57..dfefd59 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go
@@ -17,7 +17,6 @@ "go/types" "regexp" "runtime" - "slices" "sort" "strings" "sync" @@ -1789,12 +1788,6 @@ return false // 'go list' is too new for go/types } - // TODO(rfindley): remove once we no longer support building gopls with Go - // 1.20 or earlier. - if !slices.Contains(build.Default.ReleaseTags, "go1.21") && strings.Count(goVersion, ".") >= 2 { - return false // unsupported patch version - } - return true }
diff --git a/internal/versions/features.go b/internal/versions/features.go index b53f178..a5f4e32 100644 --- a/internal/versions/features.go +++ b/internal/versions/features.go
@@ -7,13 +7,17 @@ // This file contains predicates for working with file versions to // decide when a tool should consider a language feature enabled. -// GoVersions that features in x/tools can be gated to. +// named constants, to avoid misspelling const ( Go1_18 = "go1.18" Go1_19 = "go1.19" Go1_20 = "go1.20" Go1_21 = "go1.21" Go1_22 = "go1.22" + Go1_23 = "go1.23" + Go1_24 = "go1.24" + Go1_25 = "go1.25" + Go1_26 = "go1.26" ) // Future is an invalid unknown Go version sometime in the future.