gopls/internal/analysis/stdversion: set RunDespiteErrors
This change enables RunDespiteErrors, after auditing the code.
This should give more timely feedback while editing.
Also, it moves the vet/gopls common code (DisallowedSymbols)
to typesinternal.TooNewStdSymbols, out of the gopls module,
in anticipation of adding this analyzer to vet.
Updates golang/go#46136
Change-Id: I8d742bf543c9146376d43ae94f7adae3b453e471
Reviewed-on: https://go-review.googlesource.com/c/tools/+/570138
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 1b005ca..242c40e 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -739,6 +739,8 @@
through a type alias that is guarded by a Go version constraint.
+[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stdversion)
+
**Enabled by default.**
## **stringintconv**
diff --git a/gopls/internal/analysis/stdversion/stdversion.go b/gopls/internal/analysis/stdversion/stdversion.go
index 3e165ff..fa5cdf5 100644
--- a/gopls/internal/analysis/stdversion/stdversion.go
+++ b/gopls/internal/analysis/stdversion/stdversion.go
@@ -15,8 +15,7 @@
"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/gopls/internal/util/slices"
- "golang.org/x/tools/internal/stdlib"
+ "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
)
@@ -35,32 +34,34 @@
`
var Analyzer = &analysis.Analyzer{
- Name: "stdversion",
- Doc: Doc,
- Requires: []*analysis.Analyzer{inspect.Analyzer},
- Run: run,
+ Name: "stdversion",
+ Doc: Doc,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/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 !slices.Contains(build.Default.ReleaseTags, "go1.22") {
+ if !slicesContains(build.Default.ReleaseTags, "go1.22") {
return nil, nil
}
- // disallowedSymbolsMemo returns the set of standard library symbols
+ // 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
- disallowedSymbolsMemo := func(pkg *types.Package, version string) map[types.Object]string {
+ disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]string {
k := key{pkg, version}
disallowed, ok := memo[k]
if !ok {
- disallowed = DisallowedSymbols(pkg, version)
+ disallowed = typesinternal.TooNewStdSymbols(pkg, version)
memo[k] = disallowed
}
return disallowed
@@ -91,7 +92,7 @@
case *ast.Ident:
if fileVersion != "" {
if obj, ok := pass.TypesInfo.Uses[n]; ok && obj.Pkg() != nil {
- disallowed := disallowedSymbolsMemo(obj.Pkg(), fileVersion)
+ disallowed := disallowedSymbols(obj.Pkg(), fileVersion)
if minVersion, ok := disallowed[origin(obj)]; ok {
noun := "module"
if fileVersion != pkgVersion {
@@ -107,86 +108,7 @@
return nil, nil
}
-// DisallowedSymbols computes the set of package-level symbols
-// exported by pkg that are not available at the specified version.
-// The result maps each symbol to its minimum version.
-//
-// (It is exported for use in gopls' completion.)
-func DisallowedSymbols(pkg *types.Package, version string) map[types.Object]string {
- disallowed := make(map[types.Object]string)
-
- // Pass 1: package-level symbols.
- symbols := stdlib.PackageSymbols[pkg.Path()]
- for _, sym := range symbols {
- symver := sym.Version.String()
- if versions.Before(version, symver) {
- switch sym.Kind {
- case stdlib.Func, stdlib.Var, stdlib.Const, stdlib.Type:
- disallowed[pkg.Scope().Lookup(sym.Name)] = symver
- }
- }
- }
-
- // Pass 2: fields and methods.
- //
- // We allow fields and methods if their associated type is
- // disallowed, as otherwise we would report false positives
- // for compatibility shims. Consider:
- //
- // //go:build go1.22
- // type T struct { F std.Real } // correct new API
- //
- // //go:build !go1.22
- // type T struct { F fake } // shim
- // type fake struct { ... }
- // func (fake) M () {}
- //
- // These alternative declarations of T use either the std.Real
- // type, introduced in go1.22, or a fake type, for the field
- // F. (The fakery could be arbitrarily deep, involving more
- // nested fields and methods than are shown here.) Clients
- // that use the compatibility shim T will compile with any
- // version of go, whether older or newer than go1.22, but only
- // the newer version will use the std.Real implementation.
- //
- // Now consider a reference to method M in new(T).F.M() in a
- // module that requires a minimum of go1.21. The analysis may
- // occur using a version of Go higher than 1.21, selecting the
- // first version of T, so the method M is Real.M. This would
- // spuriously cause the analyzer to report a reference to a
- // too-new symbol even though this expression compiles just
- // fine (with the fake implementation) using go1.21.
- for _, sym := range symbols {
- symVersion := sym.Version.String()
- if !versions.Before(version, symVersion) {
- continue // allowed
- }
-
- var obj types.Object
- switch sym.Kind {
- case stdlib.Field:
- typename, name := sym.SplitField()
- t := pkg.Scope().Lookup(typename)
- if disallowed[t] == "" {
- obj, _, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name)
- }
-
- case stdlib.Method:
- ptr, recvname, name := sym.SplitMethod()
- t := pkg.Scope().Lookup(recvname)
- if disallowed[t] == "" {
- obj, _, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name)
- }
- }
- if obj != nil {
- disallowed[obj] = symVersion
- }
- }
-
- return disallowed
-}
-
-// Reduced from ../../golang/util.go. Good enough for now.
+// 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 {
@@ -218,3 +140,13 @@
}
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
+}
diff --git a/gopls/internal/analysis/stdversion/testdata/test.txtar b/gopls/internal/analysis/stdversion/testdata/test.txtar
index 65ee7d4..b339dc2 100644
--- a/gopls/internal/analysis/stdversion/testdata/test.txtar
+++ b/gopls/internal/analysis/stdversion/testdata/test.txtar
@@ -79,6 +79,8 @@
new(types.Package).GoVersion() // want `types.GoVersion requires go1.21 or later \(module is go1.20\)`
}
+invalid syntax // exercise RunDespiteErrors
+
-- sub/tagged.go --
//go:build go1.21
@@ -99,3 +101,4 @@
new(types.Package).GoVersion() // ok: file requires go1.21
}
+
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go
index fe66afa..1a2c7c1 100644
--- a/gopls/internal/golang/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -29,7 +29,6 @@
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/ast/astutil"
- "golang.org/x/tools/gopls/internal/analysis/stdversion"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/file"
@@ -247,7 +246,7 @@
methodSetCache map[methodSetKey]*types.MethodSet
// tooNewSymbolsCache is a cache of
- // [stdversion.DisallowedSymbols], recording for each std
+ // [typesinternal.TooNewStdSymbols], recording for each std
// package which of its exported symbols are too new for
// the version of Go in force in the completion file.
// (The value is the minimum version in the form "go1.%d".)
@@ -281,7 +280,7 @@
}
disallowed, ok := c.tooNewSymbolsCache[pkg]
if !ok {
- disallowed = stdversion.DisallowedSymbols(pkg, c.goversion)
+ disallowed = typesinternal.TooNewStdSymbols(pkg, c.goversion)
c.tooNewSymbolsCache[pkg] = disallowed
}
return disallowed[obj] != ""
diff --git a/gopls/internal/settings/api_json.go b/gopls/internal/settings/api_json.go
index c2a12e3..7a7b841 100644
--- a/gopls/internal/settings/api_json.go
+++ b/gopls/internal/settings/api_json.go
@@ -1187,6 +1187,7 @@
{
Name: "stdversion",
Doc: "report uses of too-new standard library symbols\n\nThe stdversion analyzer reports references to symbols in the standard\nlibrary that were introduced by a Go release higher than the one in\nforce in the referring file. (Recall that the file's Go version is\ndefined by the 'go' directive its module's go.mod file, or by a\n\"//go:build go1.X\" build tag at the top of the file.)\n\nThe analyzer does not report a diagnostic for a reference to a \"too\nnew\" field or method of a type that is itself \"too new\", as this may\nhave false positives, for example if fields or methods are accessed\nthrough a type alias that is guarded by a Go version constraint.\n",
+ URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stdversion",
Default: true,
},
{
diff --git a/internal/typesinternal/toonew.go b/internal/typesinternal/toonew.go
new file mode 100644
index 0000000..cc86487
--- /dev/null
+++ b/internal/typesinternal/toonew.go
@@ -0,0 +1,89 @@
+// 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 typesinternal
+
+import (
+ "go/types"
+
+ "golang.org/x/tools/internal/stdlib"
+ "golang.org/x/tools/internal/versions"
+)
+
+// TooNewStdSymbols computes the set of package-level symbols
+// exported by pkg that are not available at the specified version.
+// The result maps each symbol to its minimum version.
+//
+// The pkg is allowed to contain type errors.
+func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]string {
+ disallowed := make(map[types.Object]string)
+
+ // Pass 1: package-level symbols.
+ symbols := stdlib.PackageSymbols[pkg.Path()]
+ for _, sym := range symbols {
+ symver := sym.Version.String()
+ if versions.Before(version, symver) {
+ switch sym.Kind {
+ case stdlib.Func, stdlib.Var, stdlib.Const, stdlib.Type:
+ disallowed[pkg.Scope().Lookup(sym.Name)] = symver
+ }
+ }
+ }
+
+ // Pass 2: fields and methods.
+ //
+ // We allow fields and methods if their associated type is
+ // disallowed, as otherwise we would report false positives
+ // for compatibility shims. Consider:
+ //
+ // //go:build go1.22
+ // type T struct { F std.Real } // correct new API
+ //
+ // //go:build !go1.22
+ // type T struct { F fake } // shim
+ // type fake struct { ... }
+ // func (fake) M () {}
+ //
+ // These alternative declarations of T use either the std.Real
+ // type, introduced in go1.22, or a fake type, for the field
+ // F. (The fakery could be arbitrarily deep, involving more
+ // nested fields and methods than are shown here.) Clients
+ // that use the compatibility shim T will compile with any
+ // version of go, whether older or newer than go1.22, but only
+ // the newer version will use the std.Real implementation.
+ //
+ // Now consider a reference to method M in new(T).F.M() in a
+ // module that requires a minimum of go1.21. The analysis may
+ // occur using a version of Go higher than 1.21, selecting the
+ // first version of T, so the method M is Real.M. This would
+ // spuriously cause the analyzer to report a reference to a
+ // too-new symbol even though this expression compiles just
+ // fine (with the fake implementation) using go1.21.
+ for _, sym := range symbols {
+ symVersion := sym.Version.String()
+ if !versions.Before(version, symVersion) {
+ continue // allowed
+ }
+
+ var obj types.Object
+ switch sym.Kind {
+ case stdlib.Field:
+ typename, name := sym.SplitField()
+ if t := pkg.Scope().Lookup(typename); t != nil && disallowed[t] == "" {
+ obj, _, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name)
+ }
+
+ case stdlib.Method:
+ ptr, recvname, name := sym.SplitMethod()
+ if t := pkg.Scope().Lookup(recvname); t != nil && disallowed[t] == "" {
+ obj, _, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name)
+ }
+ }
+ if obj != nil {
+ disallowed[obj] = symVersion
+ }
+ }
+
+ return disallowed
+}