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