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  }