// Copyright 2024 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 stdversion reports uses of standard library symbols that are // "too new" for the Go version in force in the referring file. package stdversion import ( "go/ast" "go/build" "go/types" "regexp" "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/typesinternal" "golang.org/x/tools/internal/versions" ) const Doc = `report uses of too-new standard library symbols The stdversion analyzer reports references to symbols in the standard library that were introduced by a Go release higher than the one in force in the referring file. (Recall that the file's Go version is defined by the 'go' directive its module's go.mod file, or by a "//go:build go1.X" build tag at the top of the file.) The analyzer does not report a diagnostic for a reference to a "too new" field or method of a type that is itself "too new", as this may have false positives, for example if fields or methods are accessed through a type alias that is guarded by a Go version constraint. ` var Analyzer = &analysis.Analyzer{ Name: "stdversion", Doc: Doc, Requires: []*analysis.Analyzer{inspect.Analyzer}, URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdversion", RunDespiteErrors: true, Run: run, } func run(pass *analysis.Pass) (any, error) { // Prior to go1.22, versions.FileVersion returns only the // toolchain version, which is of no use to us, so // disable this analyzer on earlier versions. if !slicesContains(build.Default.ReleaseTags, "go1.22") { return nil, nil } // Don't report diagnostics for modules marked before go1.21, // since at that time the go directive wasn't clearly // specified as a toolchain requirement. // // TODO(adonovan): after go1.21, call GoVersion directly. pkgVersion := any(pass.Pkg).(interface{ GoVersion() string }).GoVersion() if !versions.AtLeast(pkgVersion, "go1.21") { return nil, nil } // disallowedSymbols returns the set of standard library symbols // in a given package that are disallowed at the specified Go version. type key struct { pkg *types.Package version string } memo := make(map[key]map[types.Object]string) // records symbol's minimum Go version disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]string { k := key{pkg, version} disallowed, ok := memo[k] if !ok { disallowed = typesinternal.TooNewStdSymbols(pkg, version) memo[k] = disallowed } return disallowed } // Scan the syntax looking for references to symbols // that are disallowed by the version of the file. inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.File)(nil), (*ast.Ident)(nil), } var fileVersion string // "" => no check inspect.Preorder(nodeFilter, func(n ast.Node) { switch n := n.(type) { case *ast.File: if isGenerated(n) { // Suppress diagnostics in generated files (such as cgo). fileVersion = "" } else { fileVersion = versions.Lang(versions.FileVersion(pass.TypesInfo, n)) // (may be "" if unknown) } case *ast.Ident: if fileVersion != "" { if obj, ok := pass.TypesInfo.Uses[n]; ok && obj.Pkg() != nil { disallowed := disallowedSymbols(obj.Pkg(), fileVersion) if minVersion, ok := disallowed[origin(obj)]; ok { noun := "module" if fileVersion != pkgVersion { noun = "file" } pass.ReportRangef(n, "%s.%s requires %v or later (%s is %s)", obj.Pkg().Name(), obj.Name(), minVersion, noun, fileVersion) } } } } }) return nil, nil } // Reduced from x/tools/gopls/internal/golang/util.go. Good enough for now. // TODO(adonovan): use ast.IsGenerated in go1.21. func isGenerated(f *ast.File) bool { for _, group := range f.Comments { for _, comment := range group.List { if matched := generatedRx.MatchString(comment.Text); matched { return true } } } return false } // Matches cgo generated comment as well as the proposed standard: // // https://golang.org/s/generatedcode var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) // origin returns the original uninstantiated symbol for obj. func origin(obj types.Object) types.Object { switch obj := obj.(type) { case *types.Var: return obj.Origin() case *types.Func: return obj.Origin() case *types.TypeName: if named, ok := obj.Type().(*types.Named); ok { // (don't unalias) return named.Origin().Obj() } } return obj } // TODO(adonovan): use go1.21 slices.Contains. func slicesContains[S ~[]E, E comparable](slice S, x E) bool { for _, elem := range slice { if elem == x { return true } } return false }