1 package st1021
2 3 import (
4 "fmt"
5 "go/ast"
6 "go/token"
7 "strings"
8 9 "honnef.co/go/tools/analysis/code"
10 "honnef.co/go/tools/analysis/facts/generated"
11 "honnef.co/go/tools/analysis/lint"
12 "honnef.co/go/tools/analysis/report"
13 14 "golang.org/x/tools/go/analysis"
15 "golang.org/x/tools/go/analysis/passes/inspect"
16 "golang.org/x/tools/go/ast/inspector"
17 )
18 19 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
20 Analyzer: &analysis.Analyzer{
21 Name: "ST1021",
22 Run: run,
23 Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer},
24 },
25 Doc: &lint.RawDocumentation{
26 Title: "The documentation of an exported type should start with type's name",
27 Text: `Doc comments work best as complete sentences, which
28 allow a wide variety of automated presentations. The first sentence
29 should be a one-sentence summary that starts with the name being
30 declared.
31 32 If every doc comment begins with the name of the item it describes,
33 you can use the \'doc\' subcommand of the \'go\' tool and run the output
34 through grep.
35 36 See https://go.dev/doc/effective_go#commentary for more
37 information on how to write good documentation.`,
38 Since: "2020.1",
39 NonDefault: true,
40 MergeIf: lint.MergeIfAny,
41 },
42 })
43 44 var Analyzer = SCAnalyzer.Analyzer
45 46 func run(pass *analysis.Pass) (interface{}, error) {
47 var genDecl *ast.GenDecl
48 fn := func(node ast.Node, push bool) bool {
49 if !push {
50 genDecl = nil
51 return false
52 }
53 if code.IsInTest(pass, node) {
54 return false
55 }
56 57 switch node := node.(type) {
58 case *ast.GenDecl:
59 if node.Tok == token.IMPORT {
60 return false
61 }
62 genDecl = node
63 return true
64 case *ast.TypeSpec:
65 if !ast.IsExported(node.Name.Name) {
66 return false
67 }
68 69 doc := node.Doc
70 text, ok := docText(doc)
71 if !ok {
72 if len(genDecl.Specs) != 1 {
73 // more than one spec in the GenDecl, don't validate the
74 // docstring
75 return false
76 }
77 if genDecl.Lparen.IsValid() {
78 // 'type ( T )' is weird, don't guess the user's intention
79 return false
80 }
81 doc = genDecl.Doc
82 text, ok = docText(doc)
83 if !ok {
84 return false
85 }
86 }
87 88 // Check comment before we strip articles in case the type's name is an article.
89 if strings.HasPrefix(text, node.Name.Name+" ") {
90 return false
91 }
92 93 s := text
94 articles := [...]string{"A", "An", "The"}
95 for _, a := range articles {
96 if strings.HasPrefix(s, a+" ") {
97 s = s[len(a)+1:]
98 break
99 }
100 }
101 if !strings.HasPrefix(s, node.Name.Name+" ") {
102 report.Report(pass, doc, fmt.Sprintf(`comment on exported type %s should be of the form "%s ..." (with optional leading article)`, node.Name.Name, node.Name.Name), report.FilterGenerated())
103 }
104 return false
105 case *ast.FuncLit, *ast.FuncDecl:
106 return false
107 default:
108 lint.ExhaustiveTypeSwitch(node)
109 return false
110 }
111 }
112 113 pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
114 return nil, nil
115 }
116 117 func docText(doc *ast.CommentGroup) (string, bool) {
118 if doc == nil {
119 return "", false
120 }
121 // We trim spaces primarily because of /**/ style comments, which often have leading space.
122 text := strings.TrimSpace(doc.Text())
123 return text, text != ""
124 }
125