1
2
3
4
5
6
7 package stdversion
8
9 import (
10 "go/ast"
11 "go/build"
12 "go/types"
13 "regexp"
14
15 "golang.org/x/tools/go/analysis"
16 "golang.org/x/tools/go/analysis/passes/inspect"
17 "golang.org/x/tools/go/ast/inspector"
18 "golang.org/x/tools/internal/typesinternal"
19 "golang.org/x/tools/internal/versions"
20 )
21
22 const Doc = `report uses of too-new standard library symbols
23
24 The stdversion analyzer reports references to symbols in the standard
25 library that were introduced by a Go release higher than the one in
26 force in the referring file. (Recall that the file's Go version is
27 defined by the 'go' directive its module's go.mod file, or by a
28 "//go:build go1.X" build tag at the top of the file.)
29
30 The analyzer does not report a diagnostic for a reference to a "too
31 new" field or method of a type that is itself "too new", as this may
32 have false positives, for example if fields or methods are accessed
33 through a type alias that is guarded by a Go version constraint.
34 `
35
36 var Analyzer = &analysis.Analyzer{
37 Name: "stdversion",
38 Doc: Doc,
39 Requires: []*analysis.Analyzer{inspect.Analyzer},
40 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdversion",
41 RunDespiteErrors: true,
42 Run: run,
43 }
44
45 func run(pass *analysis.Pass) (any, error) {
46
47
48
49 if !slicesContains(build.Default.ReleaseTags, "go1.22") {
50 return nil, nil
51 }
52
53
54
55
56
57
58 pkgVersion := any(pass.Pkg).(interface{ GoVersion() string }).GoVersion()
59 if !versions.AtLeast(pkgVersion, "go1.21") {
60 return nil, nil
61 }
62
63
64
65 type key struct {
66 pkg *types.Package
67 version string
68 }
69 memo := make(map[key]map[types.Object]string)
70 disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]string {
71 k := key{pkg, version}
72 disallowed, ok := memo[k]
73 if !ok {
74 disallowed = typesinternal.TooNewStdSymbols(pkg, version)
75 memo[k] = disallowed
76 }
77 return disallowed
78 }
79
80
81
82 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
83 nodeFilter := []ast.Node{
84 (*ast.File)(nil),
85 (*ast.Ident)(nil),
86 }
87 var fileVersion string
88 inspect.Preorder(nodeFilter, func(n ast.Node) {
89 switch n := n.(type) {
90 case *ast.File:
91 if isGenerated(n) {
92
93 fileVersion = ""
94 } else {
95 fileVersion = versions.Lang(versions.FileVersion(pass.TypesInfo, n))
96
97 }
98
99 case *ast.Ident:
100 if fileVersion != "" {
101 if obj, ok := pass.TypesInfo.Uses[n]; ok && obj.Pkg() != nil {
102 disallowed := disallowedSymbols(obj.Pkg(), fileVersion)
103 if minVersion, ok := disallowed[origin(obj)]; ok {
104 noun := "module"
105 if fileVersion != pkgVersion {
106 noun = "file"
107 }
108 pass.ReportRangef(n, "%s.%s requires %v or later (%s is %s)",
109 obj.Pkg().Name(), obj.Name(), minVersion, noun, fileVersion)
110 }
111 }
112 }
113 }
114 })
115 return nil, nil
116 }
117
118
119
120 func isGenerated(f *ast.File) bool {
121 for _, group := range f.Comments {
122 for _, comment := range group.List {
123 if matched := generatedRx.MatchString(comment.Text); matched {
124 return true
125 }
126 }
127 }
128 return false
129 }
130
131
132
133
134 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
135
136
137 func origin(obj types.Object) types.Object {
138 switch obj := obj.(type) {
139 case *types.Var:
140 return obj.Origin()
141 case *types.Func:
142 return obj.Origin()
143 case *types.TypeName:
144 if named, ok := obj.Type().(*types.Named); ok {
145 return named.Origin().Obj()
146 }
147 }
148 return obj
149 }
150
151
152 func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
153 for _, elem := range slice {
154 if elem == x {
155 return true
156 }
157 }
158 return false
159 }
160
View as plain text