Skip to content

Commit c6e02e3

Browse files
committed
gopls/internal/lsp/source: highlight deprecated symbols
This utilizes the code copied from honnef.co/tools/staticcheck. Unlike staticcheck.CheckDeprecated that assumes analyzers with `-go` flag specifying the target go version and utilizes structured knowledge of stdlib deprecated symbols, this analyzer depends on the facts and surfaces the deprecation notices as written in the comments. We also simplified implementation during review. Gopls turns this analyzer's reports to Hint/Deprecated diagnostics. Editors like VS Code strike out the marked symbols but don't surface them in the PROBLEMS panel. Fixes golang/go#40447 Change-Id: I7ac04c6008587e3c71689dec5cb4ec06523b67f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/508508 Reviewed-by: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]>
1 parent 07bfcd4 commit c6e02e3

File tree

11 files changed

+395
-1
lines changed

11 files changed

+395
-1
lines changed

gopls/doc/analyzers.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ errors is discouraged.
108108

109109
**Enabled by default.**
110110

111+
## **deprecated**
112+
113+
check for use of deprecated identifiers
114+
115+
The deprecated analyzer looks for deprecated symbols and package imports.
116+
117+
See https://go.dev/wiki/Deprecated to learn about Go's convention
118+
for documenting and signaling deprecated identifiers.
119+
120+
**Enabled by default.**
121+
111122
## **directive**
112123

113124
check Go toolchain directives such as //go:debug
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright 2023 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 deprecated defines an Analyzer that marks deprecated symbols and package imports.
6+
package deprecated
7+
8+
import (
9+
"bytes"
10+
"go/ast"
11+
"go/format"
12+
"go/token"
13+
"go/types"
14+
"strconv"
15+
"strings"
16+
17+
"golang.org/x/tools/go/analysis"
18+
"golang.org/x/tools/go/analysis/passes/inspect"
19+
"golang.org/x/tools/go/ast/inspector"
20+
"golang.org/x/tools/internal/typeparams"
21+
)
22+
23+
// TODO(hyangah): use analysisutil.MustExtractDoc.
24+
var doc = `check for use of deprecated identifiers
25+
26+
The deprecated analyzer looks for deprecated symbols and package imports.
27+
28+
See https://go.dev/wiki/Deprecated to learn about Go's convention
29+
for documenting and signaling deprecated identifiers.`
30+
31+
var Analyzer = &analysis.Analyzer{
32+
Name: "deprecated",
33+
Doc: doc,
34+
Requires: []*analysis.Analyzer{inspect.Analyzer},
35+
Run: checkDeprecated,
36+
FactTypes: []analysis.Fact{(*deprecationFact)(nil)},
37+
RunDespiteErrors: true,
38+
}
39+
40+
// checkDeprecated is a simplified copy of staticcheck.CheckDeprecated.
41+
func checkDeprecated(pass *analysis.Pass) (interface{}, error) {
42+
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
43+
44+
deprs, err := collectDeprecatedNames(pass, inspector)
45+
if err != nil || (len(deprs.packages) == 0 && len(deprs.objects) == 0) {
46+
return nil, err
47+
}
48+
49+
reportDeprecation := func(depr *deprecationFact, node ast.Node) {
50+
// TODO(hyangah): staticcheck.CheckDeprecated has more complex logic. Do we need it here?
51+
// TODO(hyangah): Scrub depr.Msg. depr.Msg may contain Go comments
52+
// markdown syntaxes but LSP diagnostics do not support markdown syntax.
53+
54+
buf := new(bytes.Buffer)
55+
if err := format.Node(buf, pass.Fset, node); err != nil {
56+
// This shouldn't happen but let's be conservative.
57+
buf.Reset()
58+
buf.WriteString("declaration")
59+
}
60+
pass.ReportRangef(node, "%s is deprecated: %s", buf, depr.Msg)
61+
}
62+
63+
nodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)}
64+
inspector.Preorder(nodeFilter, func(node ast.Node) {
65+
// Caveat: this misses dot-imported objects
66+
sel, ok := node.(*ast.SelectorExpr)
67+
if !ok {
68+
return
69+
}
70+
71+
obj := pass.TypesInfo.ObjectOf(sel.Sel)
72+
if obj_, ok := obj.(*types.Func); ok {
73+
obj = typeparams.OriginMethod(obj_)
74+
}
75+
if obj == nil || obj.Pkg() == nil {
76+
// skip invalid sel.Sel.
77+
return
78+
}
79+
80+
if obj.Pkg() == pass.Pkg {
81+
// A package is allowed to use its own deprecated objects
82+
return
83+
}
84+
85+
// A package "foo" has two related packages "foo_test" and "foo.test", for external tests and the package main
86+
// generated by 'go test' respectively. "foo_test" can import and use "foo", "foo.test" imports and uses "foo"
87+
// and "foo_test".
88+
89+
if strings.TrimSuffix(pass.Pkg.Path(), "_test") == obj.Pkg().Path() {
90+
// foo_test (the external tests of foo) can use objects from foo.
91+
return
92+
}
93+
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == obj.Pkg().Path() {
94+
// foo.test (the main package of foo's tests) can use objects from foo.
95+
return
96+
}
97+
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(obj.Pkg().Path(), "_test") {
98+
// foo.test (the main package of foo's tests) can use objects from foo's external tests.
99+
return
100+
}
101+
102+
if depr, ok := deprs.objects[obj]; ok {
103+
reportDeprecation(depr, sel)
104+
}
105+
})
106+
107+
for _, f := range pass.Files {
108+
for _, spec := range f.Imports {
109+
var imp *types.Package
110+
var obj types.Object
111+
if spec.Name != nil {
112+
obj = pass.TypesInfo.ObjectOf(spec.Name)
113+
} else {
114+
obj = pass.TypesInfo.Implicits[spec]
115+
}
116+
pkgName, ok := obj.(*types.PkgName)
117+
if !ok {
118+
continue
119+
}
120+
imp = pkgName.Imported()
121+
122+
path, err := strconv.Unquote(spec.Path.Value)
123+
if err != nil {
124+
continue
125+
}
126+
pkgPath := pass.Pkg.Path()
127+
if strings.TrimSuffix(pkgPath, "_test") == path {
128+
// foo_test can import foo
129+
continue
130+
}
131+
if strings.TrimSuffix(pkgPath, ".test") == path {
132+
// foo.test can import foo
133+
continue
134+
}
135+
if strings.TrimSuffix(pkgPath, ".test") == strings.TrimSuffix(path, "_test") {
136+
// foo.test can import foo_test
137+
continue
138+
}
139+
if depr, ok := deprs.packages[imp]; ok {
140+
reportDeprecation(depr, spec.Path)
141+
}
142+
}
143+
}
144+
return nil, nil
145+
}
146+
147+
type deprecationFact struct{ Msg string }
148+
149+
func (*deprecationFact) AFact() {}
150+
func (d *deprecationFact) String() string { return "Deprecated: " + d.Msg }
151+
152+
type deprecatedNames struct {
153+
objects map[types.Object]*deprecationFact
154+
packages map[*types.Package]*deprecationFact
155+
}
156+
157+
// collectDeprecatedNames collects deprecated identifiers and publishes
158+
// them both as Facts and the return value. This is a simplified copy
159+
// of staticcheck's fact_deprecated analyzer.
160+
func collectDeprecatedNames(pass *analysis.Pass, ins *inspector.Inspector) (deprecatedNames, error) {
161+
extractDeprecatedMessage := func(docs []*ast.CommentGroup) string {
162+
for _, doc := range docs {
163+
if doc == nil {
164+
continue
165+
}
166+
parts := strings.Split(doc.Text(), "\n\n")
167+
for _, part := range parts {
168+
if !strings.HasPrefix(part, "Deprecated: ") {
169+
continue
170+
}
171+
alt := part[len("Deprecated: "):]
172+
alt = strings.Replace(alt, "\n", " ", -1)
173+
return strings.TrimSpace(alt)
174+
}
175+
}
176+
return ""
177+
}
178+
179+
doDocs := func(names []*ast.Ident, docs *ast.CommentGroup) {
180+
alt := extractDeprecatedMessage([]*ast.CommentGroup{docs})
181+
if alt == "" {
182+
return
183+
}
184+
185+
for _, name := range names {
186+
obj := pass.TypesInfo.ObjectOf(name)
187+
pass.ExportObjectFact(obj, &deprecationFact{alt})
188+
}
189+
}
190+
191+
var docs []*ast.CommentGroup
192+
for _, f := range pass.Files {
193+
docs = append(docs, f.Doc)
194+
}
195+
if alt := extractDeprecatedMessage(docs); alt != "" {
196+
// Don't mark package syscall as deprecated, even though
197+
// it is. A lot of people still use it for simple
198+
// constants like SIGKILL, and I am not comfortable
199+
// telling them to use x/sys for that.
200+
if pass.Pkg.Path() != "syscall" {
201+
pass.ExportPackageFact(&deprecationFact{alt})
202+
}
203+
}
204+
nodeFilter := []ast.Node{
205+
(*ast.GenDecl)(nil),
206+
(*ast.FuncDecl)(nil),
207+
(*ast.TypeSpec)(nil),
208+
(*ast.ValueSpec)(nil),
209+
(*ast.File)(nil),
210+
(*ast.StructType)(nil),
211+
(*ast.InterfaceType)(nil),
212+
}
213+
ins.Preorder(nodeFilter, func(node ast.Node) {
214+
var names []*ast.Ident
215+
var docs *ast.CommentGroup
216+
switch node := node.(type) {
217+
case *ast.GenDecl:
218+
switch node.Tok {
219+
case token.TYPE, token.CONST, token.VAR:
220+
docs = node.Doc
221+
for i := range node.Specs {
222+
switch n := node.Specs[i].(type) {
223+
case *ast.ValueSpec:
224+
names = append(names, n.Names...)
225+
case *ast.TypeSpec:
226+
names = append(names, n.Name)
227+
}
228+
}
229+
default:
230+
return
231+
}
232+
case *ast.FuncDecl:
233+
docs = node.Doc
234+
names = []*ast.Ident{node.Name}
235+
case *ast.TypeSpec:
236+
docs = node.Doc
237+
names = []*ast.Ident{node.Name}
238+
case *ast.ValueSpec:
239+
docs = node.Doc
240+
names = node.Names
241+
case *ast.StructType:
242+
for _, field := range node.Fields.List {
243+
doDocs(field.Names, field.Doc)
244+
}
245+
case *ast.InterfaceType:
246+
for _, field := range node.Methods.List {
247+
doDocs(field.Names, field.Doc)
248+
}
249+
}
250+
if docs != nil && len(names) > 0 {
251+
doDocs(names, docs)
252+
}
253+
})
254+
255+
// Every identifier is potentially deprecated, so we will need
256+
// to look up facts a lot. Construct maps of all facts propagated
257+
// to this pass for fast lookup.
258+
out := deprecatedNames{
259+
objects: map[types.Object]*deprecationFact{},
260+
packages: map[*types.Package]*deprecationFact{},
261+
}
262+
for _, fact := range pass.AllObjectFacts() {
263+
out.objects[fact.Object] = fact.Fact.(*deprecationFact)
264+
}
265+
for _, fact := range pass.AllPackageFacts() {
266+
out.packages[fact.Package] = fact.Fact.(*deprecationFact)
267+
}
268+
269+
return out, nil
270+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2023 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 deprecated
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/internal/testenv"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testenv.NeedsGo1Point(t, 19)
16+
testdata := analysistest.TestData()
17+
analysistest.Run(t, testdata, Analyzer, "a")
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2023 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 usedeprecated
6+
7+
import "io/ioutil" // want "\"io/ioutil\" is deprecated: .*"
8+
9+
func x() {
10+
_, _ = ioutil.ReadFile("") // want "ioutil.ReadFile is deprecated: As of Go 1.16, .*"
11+
Legacy() // expect no deprecation notice.
12+
}
13+
14+
// Legacy is deprecated.
15+
//
16+
// Deprecated: use X instead.
17+
func Legacy() {} // want Legacy:"Deprecated: use X instead."
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2023 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 usedeprecated
6+
7+
import "testing"
8+
9+
func TestF(t *testing.T) {
10+
Legacy() // expect no deprecation notice.
11+
x()
12+
}

gopls/internal/lsp/cache/errors.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,10 +348,11 @@ func toSourceDiagnostic(srcAnalyzer *source.Analyzer, gobDiag *gobDiagnostic) *s
348348
Message: gobDiag.Message,
349349
Related: related,
350350
SuggestedFixes: fixes,
351+
Tags: srcAnalyzer.Tag,
351352
}
352353
// If the fixes only delete code, assume that the diagnostic is reporting dead code.
353354
if onlyDeletions(fixes) {
354-
diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
355+
diag.Tags = append(diag.Tags, protocol.Unnecessary)
355356
}
356357
return diag
357358
}

gopls/internal/lsp/regtest/expectation.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sort"
1111
"strings"
1212

13+
"github.com/google/go-cmp/cmp"
1314
"golang.org/x/tools/gopls/internal/lsp"
1415
"golang.org/x/tools/gopls/internal/lsp/protocol"
1516
)
@@ -774,3 +775,14 @@ func WithMessage(substring string) DiagnosticFilter {
774775
},
775776
}
776777
}
778+
779+
// WithSeverityTags filters to diagnostics whose severity and tags match
780+
// the given expectation.
781+
func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter {
782+
return DiagnosticFilter{
783+
desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags),
784+
check: func(_ string, d protocol.Diagnostic) bool {
785+
return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags)
786+
},
787+
}
788+
}

gopls/internal/lsp/source/api_json.go

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)