-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
- Loading branch information
Showing
11 changed files
with
395 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
// Copyright 2023 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 deprecated defines an Analyzer that marks deprecated symbols and package imports. | ||
package deprecated | ||
|
||
import ( | ||
"bytes" | ||
"go/ast" | ||
"go/format" | ||
"go/token" | ||
"go/types" | ||
"strconv" | ||
"strings" | ||
|
||
"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/typeparams" | ||
) | ||
|
||
// TODO(hyangah): use analysisutil.MustExtractDoc. | ||
var doc = `check for use of deprecated identifiers | ||
The deprecated analyzer looks for deprecated symbols and package imports. | ||
See https://go.dev/wiki/Deprecated to learn about Go's convention | ||
for documenting and signaling deprecated identifiers.` | ||
|
||
var Analyzer = &analysis.Analyzer{ | ||
Name: "deprecated", | ||
Doc: doc, | ||
Requires: []*analysis.Analyzer{inspect.Analyzer}, | ||
Run: checkDeprecated, | ||
FactTypes: []analysis.Fact{(*deprecationFact)(nil)}, | ||
RunDespiteErrors: true, | ||
} | ||
|
||
// checkDeprecated is a simplified copy of staticcheck.CheckDeprecated. | ||
func checkDeprecated(pass *analysis.Pass) (interface{}, error) { | ||
inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) | ||
|
||
deprs, err := collectDeprecatedNames(pass, inspector) | ||
if err != nil || (len(deprs.packages) == 0 && len(deprs.objects) == 0) { | ||
return nil, err | ||
} | ||
|
||
reportDeprecation := func(depr *deprecationFact, node ast.Node) { | ||
// TODO(hyangah): staticcheck.CheckDeprecated has more complex logic. Do we need it here? | ||
// TODO(hyangah): Scrub depr.Msg. depr.Msg may contain Go comments | ||
// markdown syntaxes but LSP diagnostics do not support markdown syntax. | ||
|
||
buf := new(bytes.Buffer) | ||
if err := format.Node(buf, pass.Fset, node); err != nil { | ||
// This shouldn't happen but let's be conservative. | ||
buf.Reset() | ||
buf.WriteString("declaration") | ||
} | ||
pass.ReportRangef(node, "%s is deprecated: %s", buf, depr.Msg) | ||
} | ||
|
||
nodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)} | ||
inspector.Preorder(nodeFilter, func(node ast.Node) { | ||
// Caveat: this misses dot-imported objects | ||
sel, ok := node.(*ast.SelectorExpr) | ||
if !ok { | ||
return | ||
} | ||
|
||
obj := pass.TypesInfo.ObjectOf(sel.Sel) | ||
if obj_, ok := obj.(*types.Func); ok { | ||
obj = typeparams.OriginMethod(obj_) | ||
} | ||
if obj == nil || obj.Pkg() == nil { | ||
// skip invalid sel.Sel. | ||
return | ||
} | ||
|
||
if obj.Pkg() == pass.Pkg { | ||
// A package is allowed to use its own deprecated objects | ||
return | ||
} | ||
|
||
// A package "foo" has two related packages "foo_test" and "foo.test", for external tests and the package main | ||
// generated by 'go test' respectively. "foo_test" can import and use "foo", "foo.test" imports and uses "foo" | ||
// and "foo_test". | ||
|
||
if strings.TrimSuffix(pass.Pkg.Path(), "_test") == obj.Pkg().Path() { | ||
// foo_test (the external tests of foo) can use objects from foo. | ||
return | ||
} | ||
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == obj.Pkg().Path() { | ||
// foo.test (the main package of foo's tests) can use objects from foo. | ||
return | ||
} | ||
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(obj.Pkg().Path(), "_test") { | ||
// foo.test (the main package of foo's tests) can use objects from foo's external tests. | ||
return | ||
} | ||
|
||
if depr, ok := deprs.objects[obj]; ok { | ||
reportDeprecation(depr, sel) | ||
} | ||
}) | ||
|
||
for _, f := range pass.Files { | ||
for _, spec := range f.Imports { | ||
var imp *types.Package | ||
var obj types.Object | ||
if spec.Name != nil { | ||
obj = pass.TypesInfo.ObjectOf(spec.Name) | ||
} else { | ||
obj = pass.TypesInfo.Implicits[spec] | ||
} | ||
pkgName, ok := obj.(*types.PkgName) | ||
if !ok { | ||
continue | ||
} | ||
imp = pkgName.Imported() | ||
|
||
path, err := strconv.Unquote(spec.Path.Value) | ||
if err != nil { | ||
continue | ||
} | ||
pkgPath := pass.Pkg.Path() | ||
if strings.TrimSuffix(pkgPath, "_test") == path { | ||
// foo_test can import foo | ||
continue | ||
} | ||
if strings.TrimSuffix(pkgPath, ".test") == path { | ||
// foo.test can import foo | ||
continue | ||
} | ||
if strings.TrimSuffix(pkgPath, ".test") == strings.TrimSuffix(path, "_test") { | ||
// foo.test can import foo_test | ||
continue | ||
} | ||
if depr, ok := deprs.packages[imp]; ok { | ||
reportDeprecation(depr, spec.Path) | ||
} | ||
} | ||
} | ||
return nil, nil | ||
} | ||
|
||
type deprecationFact struct{ Msg string } | ||
|
||
func (*deprecationFact) AFact() {} | ||
func (d *deprecationFact) String() string { return "Deprecated: " + d.Msg } | ||
|
||
type deprecatedNames struct { | ||
objects map[types.Object]*deprecationFact | ||
packages map[*types.Package]*deprecationFact | ||
} | ||
|
||
// collectDeprecatedNames collects deprecated identifiers and publishes | ||
// them both as Facts and the return value. This is a simplified copy | ||
// of staticcheck's fact_deprecated analyzer. | ||
func collectDeprecatedNames(pass *analysis.Pass, ins *inspector.Inspector) (deprecatedNames, error) { | ||
extractDeprecatedMessage := func(docs []*ast.CommentGroup) string { | ||
for _, doc := range docs { | ||
if doc == nil { | ||
continue | ||
} | ||
parts := strings.Split(doc.Text(), "\n\n") | ||
for _, part := range parts { | ||
if !strings.HasPrefix(part, "Deprecated: ") { | ||
continue | ||
} | ||
alt := part[len("Deprecated: "):] | ||
alt = strings.Replace(alt, "\n", " ", -1) | ||
return strings.TrimSpace(alt) | ||
} | ||
} | ||
return "" | ||
} | ||
|
||
doDocs := func(names []*ast.Ident, docs *ast.CommentGroup) { | ||
alt := extractDeprecatedMessage([]*ast.CommentGroup{docs}) | ||
if alt == "" { | ||
return | ||
} | ||
|
||
for _, name := range names { | ||
obj := pass.TypesInfo.ObjectOf(name) | ||
pass.ExportObjectFact(obj, &deprecationFact{alt}) | ||
} | ||
} | ||
|
||
var docs []*ast.CommentGroup | ||
for _, f := range pass.Files { | ||
docs = append(docs, f.Doc) | ||
} | ||
if alt := extractDeprecatedMessage(docs); alt != "" { | ||
// Don't mark package syscall as deprecated, even though | ||
// it is. A lot of people still use it for simple | ||
// constants like SIGKILL, and I am not comfortable | ||
// telling them to use x/sys for that. | ||
if pass.Pkg.Path() != "syscall" { | ||
pass.ExportPackageFact(&deprecationFact{alt}) | ||
} | ||
} | ||
nodeFilter := []ast.Node{ | ||
(*ast.GenDecl)(nil), | ||
(*ast.FuncDecl)(nil), | ||
(*ast.TypeSpec)(nil), | ||
(*ast.ValueSpec)(nil), | ||
(*ast.File)(nil), | ||
(*ast.StructType)(nil), | ||
(*ast.InterfaceType)(nil), | ||
} | ||
ins.Preorder(nodeFilter, func(node ast.Node) { | ||
var names []*ast.Ident | ||
var docs *ast.CommentGroup | ||
switch node := node.(type) { | ||
case *ast.GenDecl: | ||
switch node.Tok { | ||
case token.TYPE, token.CONST, token.VAR: | ||
docs = node.Doc | ||
for i := range node.Specs { | ||
switch n := node.Specs[i].(type) { | ||
case *ast.ValueSpec: | ||
names = append(names, n.Names...) | ||
case *ast.TypeSpec: | ||
names = append(names, n.Name) | ||
} | ||
} | ||
default: | ||
return | ||
} | ||
case *ast.FuncDecl: | ||
docs = node.Doc | ||
names = []*ast.Ident{node.Name} | ||
case *ast.TypeSpec: | ||
docs = node.Doc | ||
names = []*ast.Ident{node.Name} | ||
case *ast.ValueSpec: | ||
docs = node.Doc | ||
names = node.Names | ||
case *ast.StructType: | ||
for _, field := range node.Fields.List { | ||
doDocs(field.Names, field.Doc) | ||
} | ||
case *ast.InterfaceType: | ||
for _, field := range node.Methods.List { | ||
doDocs(field.Names, field.Doc) | ||
} | ||
} | ||
if docs != nil && len(names) > 0 { | ||
doDocs(names, docs) | ||
} | ||
}) | ||
|
||
// Every identifier is potentially deprecated, so we will need | ||
// to look up facts a lot. Construct maps of all facts propagated | ||
// to this pass for fast lookup. | ||
out := deprecatedNames{ | ||
objects: map[types.Object]*deprecationFact{}, | ||
packages: map[*types.Package]*deprecationFact{}, | ||
} | ||
for _, fact := range pass.AllObjectFacts() { | ||
out.objects[fact.Object] = fact.Fact.(*deprecationFact) | ||
} | ||
for _, fact := range pass.AllPackageFacts() { | ||
out.packages[fact.Package] = fact.Fact.(*deprecationFact) | ||
} | ||
|
||
return out, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// Copyright 2023 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 deprecated | ||
|
||
import ( | ||
"testing" | ||
|
||
"golang.org/x/tools/go/analysis/analysistest" | ||
"golang.org/x/tools/internal/testenv" | ||
) | ||
|
||
func Test(t *testing.T) { | ||
testenv.NeedsGo1Point(t, 19) | ||
testdata := analysistest.TestData() | ||
analysistest.Run(t, testdata, Analyzer, "a") | ||
} |
17 changes: 17 additions & 0 deletions
17
gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Copyright 2023 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 usedeprecated | ||
|
||
import "io/ioutil" // want "\"io/ioutil\" is deprecated: .*" | ||
|
||
func x() { | ||
_, _ = ioutil.ReadFile("") // want "ioutil.ReadFile is deprecated: As of Go 1.16, .*" | ||
Legacy() // expect no deprecation notice. | ||
} | ||
|
||
// Legacy is deprecated. | ||
// | ||
// Deprecated: use X instead. | ||
func Legacy() {} // want Legacy:"Deprecated: use X instead." |
12 changes: 12 additions & 0 deletions
12
gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright 2023 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 usedeprecated | ||
|
||
import "testing" | ||
|
||
func TestF(t *testing.T) { | ||
Legacy() // expect no deprecation notice. | ||
x() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.