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