aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/vendor/golang.org/x/tools/go/analysis/passes/stdversion/stdversion.go
blob: 75d8697759eb62892b84e33c1612a6ac76ebfe5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// 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
}