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. 7package stdversion 8 9import ( 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 22const Doc = `report uses of too-new standard library symbols 23 24The stdversion analyzer reports references to symbols in the standard 25library that were introduced by a Go release higher than the one in 26force in the referring file. (Recall that the file's Go version is 27defined 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 30The analyzer does not report a diagnostic for a reference to a "too 31new" field or method of a type that is itself "too new", as this may 32have false positives, for example if fields or methods are accessed 33through a type alias that is guarded by a Go version constraint. 34` 35 36var 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 45func 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. 120func 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 134var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) 135 136// origin returns the original uninstantiated symbol for obj. 137func 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. 152func 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