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  }