st1022.go raw
1 package st1022
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: "ST1022",
22 Run: run,
23 Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer},
24 },
25 Doc: &lint.RawDocumentation{
26 Title: "The documentation of an exported variable or constant should start with variable'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.ValueSpec:
65 if genDecl.Lparen.IsValid() || len(node.Names) > 1 {
66 // Don't try to guess the user's intention
67 return false
68 }
69 name := node.Names[0].Name
70 if !ast.IsExported(name) {
71 return false
72 }
73 text, ok := docText(genDecl.Doc)
74 if !ok {
75 return false
76 }
77 prefix := name + " "
78 if !strings.HasPrefix(text, prefix) {
79 kind := "var"
80 if genDecl.Tok == token.CONST {
81 kind = "const"
82 }
83 report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated())
84 }
85 return false
86 case *ast.FuncLit, *ast.FuncDecl:
87 return false
88 default:
89 lint.ExhaustiveTypeSwitch(node)
90 return false
91 }
92 }
93
94 pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
95 return nil, nil
96 }
97
98 func docText(doc *ast.CommentGroup) (string, bool) {
99 if doc == nil {
100 return "", false
101 }
102 // We trim spaces primarily because of /**/ style comments, which often have leading space.
103 text := strings.TrimSpace(doc.Text())
104 return text, text != ""
105 }
106