github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/stylecheck/st1021/st1021.go (about) 1 package st1021 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "strings" 8 9 "github.com/amarpal/go-tools/analysis/code" 10 "github.com/amarpal/go-tools/analysis/facts/generated" 11 "github.com/amarpal/go-tools/analysis/lint" 12 "github.com/amarpal/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.Documentation{ 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://golang.org/doc/effective_go.html#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 }