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