You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

exported.go 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package rule
  2. import (
  3. "fmt"
  4. "go/ast"
  5. "go/token"
  6. "strings"
  7. "unicode"
  8. "unicode/utf8"
  9. "github.com/mgechev/revive/lint"
  10. )
  11. // ExportedRule lints given else constructs.
  12. type ExportedRule struct{}
  13. // Apply applies the rule to given file.
  14. func (r *ExportedRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
  15. var failures []lint.Failure
  16. if isTest(file) {
  17. return failures
  18. }
  19. fileAst := file.AST
  20. walker := lintExported{
  21. file: file,
  22. fileAst: fileAst,
  23. onFailure: func(failure lint.Failure) {
  24. failures = append(failures, failure)
  25. },
  26. genDeclMissingComments: make(map[*ast.GenDecl]bool),
  27. }
  28. ast.Walk(&walker, fileAst)
  29. return failures
  30. }
  31. // Name returns the rule name.
  32. func (r *ExportedRule) Name() string {
  33. return "exported"
  34. }
  35. type lintExported struct {
  36. file *lint.File
  37. fileAst *ast.File
  38. lastGen *ast.GenDecl
  39. genDeclMissingComments map[*ast.GenDecl]bool
  40. onFailure func(lint.Failure)
  41. }
  42. func (w *lintExported) lintFuncDoc(fn *ast.FuncDecl) {
  43. if !ast.IsExported(fn.Name.Name) {
  44. // func is unexported
  45. return
  46. }
  47. kind := "function"
  48. name := fn.Name.Name
  49. if fn.Recv != nil && len(fn.Recv.List) > 0 {
  50. // method
  51. kind = "method"
  52. recv := receiverType(fn)
  53. if !ast.IsExported(recv) {
  54. // receiver is unexported
  55. return
  56. }
  57. if commonMethods[name] {
  58. return
  59. }
  60. switch name {
  61. case "Len", "Less", "Swap":
  62. if w.file.Pkg.Sortable[recv] {
  63. return
  64. }
  65. }
  66. name = recv + "." + name
  67. }
  68. if fn.Doc == nil {
  69. w.onFailure(lint.Failure{
  70. Node: fn,
  71. Confidence: 1,
  72. Category: "comments",
  73. Failure: fmt.Sprintf("exported %s %s should have comment or be unexported", kind, name),
  74. })
  75. return
  76. }
  77. s := normalizeText(fn.Doc.Text())
  78. prefix := fn.Name.Name + " "
  79. if !strings.HasPrefix(s, prefix) {
  80. w.onFailure(lint.Failure{
  81. Node: fn.Doc,
  82. Confidence: 0.8,
  83. Category: "comments",
  84. Failure: fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix),
  85. })
  86. }
  87. }
  88. func (w *lintExported) checkStutter(id *ast.Ident, thing string) {
  89. pkg, name := w.fileAst.Name.Name, id.Name
  90. if !ast.IsExported(name) {
  91. // unexported name
  92. return
  93. }
  94. // A name stutters if the package name is a strict prefix
  95. // and the next character of the name starts a new word.
  96. if len(name) <= len(pkg) {
  97. // name is too short to stutter.
  98. // This permits the name to be the same as the package name.
  99. return
  100. }
  101. if !strings.EqualFold(pkg, name[:len(pkg)]) {
  102. return
  103. }
  104. // We can assume the name is well-formed UTF-8.
  105. // If the next rune after the package name is uppercase or an underscore
  106. // the it's starting a new word and thus this name stutters.
  107. rem := name[len(pkg):]
  108. if next, _ := utf8.DecodeRuneInString(rem); next == '_' || unicode.IsUpper(next) {
  109. w.onFailure(lint.Failure{
  110. Node: id,
  111. Confidence: 0.8,
  112. Category: "naming",
  113. Failure: fmt.Sprintf("%s name will be used as %s.%s by other packages, and that stutters; consider calling this %s", thing, pkg, name, rem),
  114. })
  115. }
  116. }
  117. func (w *lintExported) lintTypeDoc(t *ast.TypeSpec, doc *ast.CommentGroup) {
  118. if !ast.IsExported(t.Name.Name) {
  119. return
  120. }
  121. if doc == nil {
  122. w.onFailure(lint.Failure{
  123. Node: t,
  124. Confidence: 1,
  125. Category: "comments",
  126. Failure: fmt.Sprintf("exported type %v should have comment or be unexported", t.Name),
  127. })
  128. return
  129. }
  130. s := normalizeText(doc.Text())
  131. articles := [...]string{"A", "An", "The", "This"}
  132. for _, a := range articles {
  133. if t.Name.Name == a {
  134. continue
  135. }
  136. if strings.HasPrefix(s, a+" ") {
  137. s = s[len(a)+1:]
  138. break
  139. }
  140. }
  141. if !strings.HasPrefix(s, t.Name.Name+" ") {
  142. w.onFailure(lint.Failure{
  143. Node: doc,
  144. Confidence: 1,
  145. Category: "comments",
  146. Failure: fmt.Sprintf(`comment on exported type %v should be of the form "%v ..." (with optional leading article)`, t.Name, t.Name),
  147. })
  148. }
  149. }
  150. func (w *lintExported) lintValueSpecDoc(vs *ast.ValueSpec, gd *ast.GenDecl, genDeclMissingComments map[*ast.GenDecl]bool) {
  151. kind := "var"
  152. if gd.Tok == token.CONST {
  153. kind = "const"
  154. }
  155. if len(vs.Names) > 1 {
  156. // Check that none are exported except for the first.
  157. for _, n := range vs.Names[1:] {
  158. if ast.IsExported(n.Name) {
  159. w.onFailure(lint.Failure{
  160. Category: "comments",
  161. Confidence: 1,
  162. Failure: fmt.Sprintf("exported %s %s should have its own declaration", kind, n.Name),
  163. Node: vs,
  164. })
  165. return
  166. }
  167. }
  168. }
  169. // Only one name.
  170. name := vs.Names[0].Name
  171. if !ast.IsExported(name) {
  172. return
  173. }
  174. if vs.Doc == nil && gd.Doc == nil {
  175. if genDeclMissingComments[gd] {
  176. return
  177. }
  178. block := ""
  179. if kind == "const" && gd.Lparen.IsValid() {
  180. block = " (or a comment on this block)"
  181. }
  182. w.onFailure(lint.Failure{
  183. Confidence: 1,
  184. Node: vs,
  185. Category: "comments",
  186. Failure: fmt.Sprintf("exported %s %s should have comment%s or be unexported", kind, name, block),
  187. })
  188. genDeclMissingComments[gd] = true
  189. return
  190. }
  191. // If this GenDecl has parens and a comment, we don't check its comment form.
  192. if gd.Lparen.IsValid() && gd.Doc != nil {
  193. return
  194. }
  195. // The relevant text to check will be on either vs.Doc or gd.Doc.
  196. // Use vs.Doc preferentially.
  197. doc := vs.Doc
  198. if doc == nil {
  199. doc = gd.Doc
  200. }
  201. prefix := name + " "
  202. s := normalizeText(doc.Text())
  203. if !strings.HasPrefix(s, prefix) {
  204. w.onFailure(lint.Failure{
  205. Confidence: 1,
  206. Node: doc,
  207. Category: "comments",
  208. Failure: fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix),
  209. })
  210. }
  211. }
  212. // normalizeText is a helper function that normalizes comment strings by:
  213. // * removing one leading space
  214. //
  215. // This function is needed because ast.CommentGroup.Text() does not handle //-style and /*-style comments uniformly
  216. func normalizeText(t string) string {
  217. return strings.TrimPrefix(t, " ")
  218. }
  219. func (w *lintExported) Visit(n ast.Node) ast.Visitor {
  220. switch v := n.(type) {
  221. case *ast.GenDecl:
  222. if v.Tok == token.IMPORT {
  223. return nil
  224. }
  225. // token.CONST, token.TYPE or token.VAR
  226. w.lastGen = v
  227. return w
  228. case *ast.FuncDecl:
  229. w.lintFuncDoc(v)
  230. if v.Recv == nil {
  231. // Only check for stutter on functions, not methods.
  232. // Method names are not used package-qualified.
  233. w.checkStutter(v.Name, "func")
  234. }
  235. // Don't proceed inside funcs.
  236. return nil
  237. case *ast.TypeSpec:
  238. // inside a GenDecl, which usually has the doc
  239. doc := v.Doc
  240. if doc == nil {
  241. doc = w.lastGen.Doc
  242. }
  243. w.lintTypeDoc(v, doc)
  244. w.checkStutter(v.Name, "type")
  245. // Don't proceed inside types.
  246. return nil
  247. case *ast.ValueSpec:
  248. w.lintValueSpecDoc(v, w.lastGen, w.genDeclMissingComments)
  249. return nil
  250. }
  251. return w
  252. }