golang.org/x/tools@v0.21.0/go/analysis/passes/stdversion/stdversion.go (about) 1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package stdversion reports uses of standard library symbols that are 6 // "too new" for the Go version in force in the referring file. 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 // Prior to go1.22, versions.FileVersion returns only the 47 // toolchain version, which is of no use to us, so 48 // disable this analyzer on earlier versions. 49 if !slicesContains(build.Default.ReleaseTags, "go1.22") { 50 return nil, nil 51 } 52 53 // Don't report diagnostics for modules marked before go1.21, 54 // since at that time the go directive wasn't clearly 55 // specified as a toolchain requirement. 56 // 57 // TODO(adonovan): after go1.21, call GoVersion directly. 58 pkgVersion := any(pass.Pkg).(interface{ GoVersion() string }).GoVersion() 59 if !versions.AtLeast(pkgVersion, "go1.21") { 60 return nil, nil 61 } 62 63 // disallowedSymbols returns the set of standard library symbols 64 // in a given package that are disallowed at the specified Go version. 65 type key struct { 66 pkg *types.Package 67 version string 68 } 69 memo := make(map[key]map[types.Object]string) // records symbol's minimum Go version 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 // Scan the syntax looking for references to symbols 81 // that are disallowed by the version of the file. 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 // "" => no check 88 inspect.Preorder(nodeFilter, func(n ast.Node) { 89 switch n := n.(type) { 90 case *ast.File: 91 if isGenerated(n) { 92 // Suppress diagnostics in generated files (such as cgo). 93 fileVersion = "" 94 } else { 95 fileVersion = versions.Lang(versions.FileVersion(pass.TypesInfo, n)) 96 // (may be "" if unknown) 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 // Reduced from x/tools/gopls/internal/golang/util.go. Good enough for now. 119 // TODO(adonovan): use ast.IsGenerated in go1.21. 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 // Matches cgo generated comment as well as the proposed standard: 132 // 133 // https://golang.org/s/generatedcode 134 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) 135 136 // origin returns the original uninstantiated symbol for obj. 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 { // (don't unalias) 145 return named.Origin().Obj() 146 } 147 } 148 return obj 149 } 150 151 // TODO(adonovan): use go1.21 slices.Contains. 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 }