Skip to content

Commit

Permalink
gopls/internal/lsp/source: highlight deprecated symbols
Browse files Browse the repository at this point in the history
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
hyangah committed Jul 25, 2023
1 parent 07bfcd4 commit c6e02e3
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 1 deletion.
11 changes: 11 additions & 0 deletions gopls/doc/analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ errors is discouraged.

**Enabled by default.**

## **deprecated**

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.

**Enabled by default.**

## **directive**

check Go toolchain directives such as //go:debug
Expand Down
270 changes: 270 additions & 0 deletions gopls/internal/lsp/analysis/deprecated/deprecated.go
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
}
18 changes: 18 additions & 0 deletions gopls/internal/lsp/analysis/deprecated/deprecated_test.go
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 gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go
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 gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go
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()
}
3 changes: 2 additions & 1 deletion gopls/internal/lsp/cache/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,10 +348,11 @@ func toSourceDiagnostic(srcAnalyzer *source.Analyzer, gobDiag *gobDiagnostic) *s
Message: gobDiag.Message,
Related: related,
SuggestedFixes: fixes,
Tags: srcAnalyzer.Tag,
}
// If the fixes only delete code, assume that the diagnostic is reporting dead code.
if onlyDeletions(fixes) {
diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
diag.Tags = append(diag.Tags, protocol.Unnecessary)
}
return diag
}
Expand Down
12 changes: 12 additions & 0 deletions gopls/internal/lsp/regtest/expectation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sort"
"strings"

"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/lsp"
"golang.org/x/tools/gopls/internal/lsp/protocol"
)
Expand Down Expand Up @@ -774,3 +775,14 @@ func WithMessage(substring string) DiagnosticFilter {
},
}
}

// WithSeverityTags filters to diagnostics whose severity and tags match
// the given expectation.
func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter {
return DiagnosticFilter{
desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags),
check: func(_ string, d protocol.Diagnostic) bool {
return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags)
},
}
}
10 changes: 10 additions & 0 deletions gopls/internal/lsp/source/api_json.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c6e02e3

Please sign in to comment.