| // 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 |
| } |