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