1 package st1020
2 3 import (
4 "fmt"
5 "go/ast"
6 "strings"
7 8 "honnef.co/go/tools/analysis/code"
9 "honnef.co/go/tools/analysis/facts/generated"
10 "honnef.co/go/tools/analysis/lint"
11 "honnef.co/go/tools/analysis/report"
12 13 "golang.org/x/tools/go/analysis"
14 "golang.org/x/tools/go/analysis/passes/inspect"
15 )
16 17 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
18 Analyzer: &analysis.Analyzer{
19 Name: "ST1020",
20 Run: run,
21 Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer},
22 },
23 Doc: &lint.RawDocumentation{
24 Title: "The documentation of an exported function should start with the function's name",
25 Text: `Doc comments work best as complete sentences, which
26 allow a wide variety of automated presentations. The first sentence
27 should be a one-sentence summary that starts with the name being
28 declared.
29 30 If every doc comment begins with the name of the item it describes,
31 you can use the \'doc\' subcommand of the \'go\' tool and run the output
32 through grep.
33 34 See https://go.dev/doc/effective_go#commentary for more
35 information on how to write good documentation.`,
36 Since: "2020.1",
37 NonDefault: true,
38 MergeIf: lint.MergeIfAny,
39 },
40 })
41 42 var Analyzer = SCAnalyzer.Analyzer
43 44 func run(pass *analysis.Pass) (interface{}, error) {
45 fn := func(node ast.Node) {
46 if code.IsInTest(pass, node) {
47 return
48 }
49 50 decl := node.(*ast.FuncDecl)
51 text, ok := docText(decl.Doc)
52 if !ok {
53 return
54 }
55 if !ast.IsExported(decl.Name.Name) {
56 return
57 }
58 if strings.HasPrefix(text, "Deprecated: ") {
59 return
60 }
61 62 kind := "function"
63 if decl.Recv != nil {
64 kind = "method"
65 var ident *ast.Ident
66 T := decl.Recv.List[0].Type
67 if T_, ok := T.(*ast.StarExpr); ok {
68 T = T_.X
69 }
70 switch T := T.(type) {
71 case *ast.IndexExpr:
72 ident = T.X.(*ast.Ident)
73 case *ast.IndexListExpr:
74 ident = T.X.(*ast.Ident)
75 case *ast.Ident:
76 ident = T
77 default:
78 lint.ExhaustiveTypeSwitch(T)
79 }
80 if !ast.IsExported(ident.Name) {
81 return
82 }
83 }
84 prefix := decl.Name.Name + " "
85 if !strings.HasPrefix(text, prefix) {
86 report.Report(pass, decl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, decl.Name.Name, prefix), report.FilterGenerated())
87 }
88 }
89 90 code.Preorder(pass, fn, (*ast.FuncDecl)(nil))
91 return nil, nil
92 }
93 94 func docText(doc *ast.CommentGroup) (string, bool) {
95 if doc == nil {
96 return "", false
97 }
98 // We trim spaces primarily because of /**/ style comments, which often have leading space.
99 text := strings.TrimSpace(doc.Text())
100 return text, text != ""
101 }
102