blob: 1a8c4c567669c6672375760eae1bb9d9be0fd6d7 [file] [log] [blame]
// 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 (
"bytes"
"go/ast"
"go/format"
"go/token"
"go/types"
"strconv"
"strings"
_ "embed"
"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/analysisinternal"
)
//go:embed doc.go
var doc string
var Analyzer = &analysis.Analyzer{
Name: "deprecated",
Doc: analysisinternal.MustExtractDoc(doc, "deprecated"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: checkDeprecated,
FactTypes: []analysis.Fact{(*deprecationFact)(nil)},
RunDespiteErrors: true,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/deprecated",
}
// 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 fn, ok := obj.(*types.Func); ok {
obj = fn.Origin()
}
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
}