internal/analysisinternal/analyzerutil: FileUsesGoVersion
Factor and share the three extant copies of this function.
Also, rename modernize.filesUsing to ...GoVersion for consistency.
Change-Id: I393caa3220172afb6c5cfca0b3c80f2ad239f2b7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/719340
Auto-Submit: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go
index f95f526..783fd35 100644
--- a/go/analysis/passes/loopclosure/loopclosure.go
+++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -14,8 +14,6 @@
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal/analyzerutil"
- "golang.org/x/tools/internal/packagepath"
- "golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
)
@@ -57,7 +55,7 @@
switch n := n.(type) {
case *ast.File:
// Only traverse the file if its goversion is strictly before go1.22.
- return !fileUsesVersion(pass, n, versions.Go1_22)
+ return !analyzerutil.FileUsesGoVersion(pass, n, versions.Go1_22)
case *ast.RangeStmt:
body = n.Body
@@ -373,30 +371,3 @@
_, 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 b1a1940..055b200 100644
--- a/go/analysis/passes/modernize/any.go
+++ b/go/analysis/passes/modernize/any.go
@@ -29,7 +29,7 @@
func runAny(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
- for curFile := range filesUsing(pass, versions.Go1_18) {
+ for curFile := range filesUsingGoVersion(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 c1d4c24..4a66da1 100644
--- a/go/analysis/passes/modernize/bloop.go
+++ b/go/analysis/passes/modernize/bloop.go
@@ -102,7 +102,7 @@
(*ast.ForStmt)(nil),
(*ast.RangeStmt)(nil),
}
- for curFile := range filesUsing(pass, versions.Go1_24) {
+ for curFile := range filesUsingGoVersion(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 7adfd4f..d3c0776 100644
--- a/go/analysis/passes/modernize/errorsastype.go
+++ b/go/analysis/passes/modernize/errorsastype.go
@@ -98,7 +98,7 @@
}
file := astutil.EnclosingFile(curDeclStmt)
- if !fileUsesVersion(pass, file, versions.Go1_26) {
+ if !analyzerutil.FileUsesGoVersion(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 ee2620f..3b6aa92 100644
--- a/go/analysis/passes/modernize/fmtappendf.go
+++ b/go/analysis/passes/modernize/fmtappendf.go
@@ -51,7 +51,7 @@
conv := curCall.Parent().Node().(*ast.CallExpr)
tv := pass.TypesInfo.Types[conv.Fun]
if tv.IsType() && types.Identical(tv.Type, byteSliceType) &&
- fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) {
+ analyzerutil.FileUsesGoVersion(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 57eb014..2b0d51c 100644
--- a/go/analysis/passes/modernize/forvar.go
+++ b/go/analysis/passes/modernize/forvar.go
@@ -39,13 +39,13 @@
// where the two idents are the same,
// and the ident is defined (:=) as a variable in the for statement.
// (Note that this 'fix' does not work for three clause loops
-// because the Go specification says "The variable used by each subsequent iteration
+// because the Go specfilesUsingGoVersionsays "The variable used by each subsequent iteration
// is declared implicitly before executing the post statement and initialized to the
// value of the previous iteration's variable at that moment.")
func forvar(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
- for curFile := range filesUsing(pass, versions.Go1_22) {
+ for curFile := range filesUsingGoVersion(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 9bccedc..b98deb1 100644
--- a/go/analysis/passes/modernize/maps.go
+++ b/go/analysis/passes/modernize/maps.go
@@ -224,7 +224,7 @@
}
// Find all range loops around m[k] = v.
- for curFile := range filesUsing(pass, versions.Go1_23) {
+ for curFile := range filesUsingGoVersion(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 0b5c92d..457f5ab 100644
--- a/go/analysis/passes/modernize/minmax.go
+++ b/go/analysis/passes/modernize/minmax.go
@@ -202,7 +202,7 @@
// Find all "if a < b { lhs = rhs }" statements.
info := pass.TypesInfo
- for curFile := range filesUsing(pass, versions.Go1_21) {
+ for curFile := range filesUsingGoVersion(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 27c07ac..f69a1ca 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -19,13 +19,13 @@
"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/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/packagepath"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/typesinternal"
- "golang.org/x/tools/internal/versions"
)
//go:embed doc.go
@@ -93,54 +93,28 @@
return info.Types[e].Value == constant.MakeInt64(n)
}
-// filesUsing returns a cursor for each *ast.File in the inspector
+// filesUsingGoVersion 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 [fileUsesGoVersion].
-// See "Tip" at [fileUsesGoVersion] for motivation.
-func filesUsing(pass *analysis.Pass, version string) iter.Seq[inspector.Cursor] {
+// approach of [fmtappendf], which uses typeindex and
+// [analyzerutil.FileUsesGoVersion]; see "Tip" documented at the
+// latter function for motivation.
+func filesUsingGoVersion(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 fileUsesVersion(pass, file, version) && !yield(curFile) {
+ if analyzerutil.FileUsesGoVersion(pass, file, version) && !yield(curFile) {
break
}
}
}
}
-// 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
-}
-
// within reports whether the current pass is analyzing one of the
// specified standard packages or their dependencies.
func within(pass *analysis.Pass, pkgs ...string) bool {
diff --git a/go/analysis/passes/modernize/newexpr.go b/go/analysis/passes/modernize/newexpr.go
index f984dab..ed8e3be 100644
--- a/go/analysis/passes/modernize/newexpr.go
+++ b/go/analysis/passes/modernize/newexpr.go
@@ -61,7 +61,7 @@
// Check file version.
file := astutil.EnclosingFile(curFuncDecl)
- if !fileUsesVersion(pass, file, versions.Go1_26) {
+ if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue // new(expr) not available in this file
}
@@ -134,7 +134,7 @@
// Check file version.
file := astutil.EnclosingFile(curCall)
- if !fileUsesVersion(pass, file, versions.Go1_26) {
+ if !analyzerutil.FileUsesGoVersion(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 0c14d4e..f5a64ca 100644
--- a/go/analysis/passes/modernize/omitzero.go
+++ b/go/analysis/passes/modernize/omitzero.go
@@ -95,13 +95,13 @@
}
// The omitzero pass searches for instances of "omitempty" in a json field tag on a
-// struct. Since "omitempty" does not have any effect when applied to a struct field,
+// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field,
// it suggests either deleting "omitempty" or replacing it with "omitzero", which
// correctly excludes structs from a json encoding.
func omitzero(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
- for curFile := range filesUsing(pass, versions.Go1_24) {
+ for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
for curStruct := range curFile.Preorder((*ast.StructType)(nil)) {
for _, curField := range curStruct.Node().(*ast.StructType).Fields.List {
checkOmitEmptyField(pass, pass.TypesInfo, curField)
diff --git a/go/analysis/passes/modernize/plusbuild.go b/go/analysis/passes/modernize/plusbuild.go
index 9b3cd2e..d3871d3 100644
--- a/go/analysis/passes/modernize/plusbuild.go
+++ b/go/analysis/passes/modernize/plusbuild.go
@@ -29,7 +29,7 @@
func plusbuild(pass *analysis.Pass) (any, error) {
check := func(f *ast.File) {
- if !fileUsesVersion(pass, f, versions.Go1_18) {
+ if !analyzerutil.FileUsesGoVersion(pass, f, versions.Go1_18) {
return
}
diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go
index 843a8a2..4841054 100644
--- a/go/analysis/passes/modernize/rangeint.go
+++ b/go/analysis/passes/modernize/rangeint.go
@@ -74,7 +74,7 @@
typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
)
- for curFile := range filesUsing(pass, versions.Go1_22) {
+ for curFile := range filesUsingGoVersion(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 e7c68b4..5292398 100644
--- a/go/analysis/passes/modernize/reflect.go
+++ b/go/analysis/passes/modernize/reflect.go
@@ -89,7 +89,7 @@
}
file := astutil.EnclosingFile(curCall)
- if !fileUsesVersion(pass, file, versions.Go1_22) {
+ if !analyzerutil.FileUsesGoVersion(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 f3c2ed2..7d08a33 100644
--- a/go/analysis/passes/modernize/slices.go
+++ b/go/analysis/passes/modernize/slices.go
@@ -205,7 +205,7 @@
skip := make(map[*ast.CallExpr]bool)
// Visit calls of form append(x, y...).
- for curFile := range filesUsing(pass, versions.Go1_21) {
+ for curFile := range filesUsingGoVersion(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 097a9ff..ed2e3e3 100644
--- a/go/analysis/passes/modernize/slicescontains.go
+++ b/go/analysis/passes/modernize/slicescontains.go
@@ -386,7 +386,7 @@
}
}
- for curFile := range filesUsing(pass, versions.Go1_21) {
+ for curFile := range filesUsingGoVersion(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 3860efe..98282e4 100644
--- a/go/analysis/passes/modernize/slicesdelete.go
+++ b/go/analysis/passes/modernize/slicesdelete.go
@@ -129,7 +129,7 @@
}},
})
}
- for curFile := range filesUsing(pass, versions.Go1_21) {
+ for curFile := range filesUsingGoVersion(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 2d66d02..20af422 100644
--- a/go/analysis/passes/modernize/sortslice.go
+++ b/go/analysis/passes/modernize/sortslice.go
@@ -88,7 +88,7 @@
}
file := astutil.EnclosingFile(curCall)
if isIndex(compare.X, i) && isIndex(compare.Y, j) &&
- fileUsesVersion(pass, file, versions.Go1_21) {
+ analyzerutil.FileUsesGoVersion(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 e35bab8..cf88443 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 !fileUsesVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) {
+ } else if !analyzerutil.FileUsesGoVersion(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 fdf47bb..dd0dc81 100644
--- a/go/analysis/passes/modernize/stringscut.go
+++ b/go/analysis/passes/modernize/stringscut.go
@@ -132,7 +132,7 @@
nextcall:
for curCall := range index.Calls(obj) {
// Check file version.
- if !fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
+ if !analyzerutil.FileUsesGoVersion(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 b76a36b..2315e61 100644
--- a/go/analysis/passes/modernize/stringscutprefix.go
+++ b/go/analysis/passes/modernize/stringscutprefix.go
@@ -71,7 +71,7 @@
return nil, nil
}
- for curFile := range filesUsing(pass, versions.Go1_20) {
+ for curFile := range filesUsingGoVersion(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 96cffa6..5dbb709 100644
--- a/go/analysis/passes/modernize/stringsseq.go
+++ b/go/analysis/passes/modernize/stringsseq.go
@@ -63,7 +63,7 @@
return nil, nil
}
- for curFile := range filesUsing(pass, versions.Go1_24) {
+ for curFile := range filesUsingGoVersion(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 19cc825..1cf4a9d 100644
--- a/go/analysis/passes/modernize/testingcontext.go
+++ b/go/analysis/passes/modernize/testingcontext.go
@@ -138,7 +138,7 @@
testObj = isTestFn(info, n)
}
}
- if testObj != nil && fileUsesVersion(pass, astutil.EnclosingFile(cur), versions.Go1_24) {
+ if testObj != nil && analyzerutil.FileUsesGoVersion(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 2844ad9..741cbc3 100644
--- a/go/analysis/passes/modernize/waitgroup.go
+++ b/go/analysis/passes/modernize/waitgroup.go
@@ -129,7 +129,7 @@
}
file := astutil.EnclosingFile(curAddCall)
- if !fileUsesVersion(pass, file, versions.Go1_25) {
+ if !analyzerutil.FileUsesGoVersion(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 a8a6495..699d2ec 100644
--- a/gopls/internal/analysis/maprange/maprange.go
+++ b/gopls/internal/analysis/maprange/maprange.go
@@ -16,8 +16,6 @@
"golang.org/x/tools/internal/analysisinternal/analyzerutil"
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"
)
@@ -93,7 +91,7 @@
// Replace with: for i := range len(m)
// (This requires Go 1.22.)
replace = "len"
- if !fileUsesVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_22) {
+ if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_22) {
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
@@ -150,30 +148,3 @@
ident, ok := expr.(*ast.Ident)
return expr != nil && (!ok || ident.Name != "_")
}
-
-// 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/internal/analysisinternal/analyzerutil/version.go b/internal/analysisinternal/analyzerutil/version.go
new file mode 100644
index 0000000..7336012
--- /dev/null
+++ b/internal/analysisinternal/analyzerutil/version.go
@@ -0,0 +1,39 @@
+// Copyright 2025 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 analyzerutil
+
+import (
+ "go/ast"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/internal/packagepath"
+ "golang.org/x/tools/internal/stdlib"
+ "golang.org/x/tools/internal/versions"
+)
+
+// FileUsesGoVersion 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.
+func FileUsesGoVersion(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
+}