2018-01-22 04:04:41 +02:00
|
|
|
package rule
|
2017-11-27 05:19:41 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
|
|
|
"go/token"
|
|
|
|
"strings"
|
2022-04-10 09:06:59 +02:00
|
|
|
"sync"
|
2017-11-27 05:19:41 +02:00
|
|
|
"unicode"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
2022-06-18 18:47:53 +02:00
|
|
|
"github.com/mgechev/revive/internal/typeparams"
|
2018-01-25 01:44:03 +02:00
|
|
|
"github.com/mgechev/revive/lint"
|
2017-11-27 05:19:41 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// ExportedRule lints given else constructs.
|
2021-10-17 20:34:48 +02:00
|
|
|
type ExportedRule struct {
|
|
|
|
configured bool
|
|
|
|
checkPrivateReceivers bool
|
|
|
|
disableStutteringCheck bool
|
2024-07-30 10:36:03 +02:00
|
|
|
checkPublicInterface bool
|
2021-10-17 20:34:48 +02:00
|
|
|
stuttersMsg string
|
2022-04-10 09:06:59 +02:00
|
|
|
sync.Mutex
|
2021-10-17 20:34:48 +02:00
|
|
|
}
|
2017-11-27 05:19:41 +02:00
|
|
|
|
2022-04-10 09:06:59 +02:00
|
|
|
func (r *ExportedRule) configure(arguments lint.Arguments) {
|
|
|
|
r.Lock()
|
2024-07-30 10:36:03 +02:00
|
|
|
defer r.Unlock()
|
2021-10-17 20:34:48 +02:00
|
|
|
if !r.configured {
|
|
|
|
r.stuttersMsg = "stutters"
|
2024-07-30 10:36:03 +02:00
|
|
|
for _, flag := range arguments {
|
|
|
|
flagStr, ok := flag.(string)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("Invalid argument for the %s rule: expecting a string, got %T", r.Name(), flag))
|
|
|
|
}
|
|
|
|
switch flagStr {
|
|
|
|
case "checkPrivateReceivers":
|
|
|
|
r.checkPrivateReceivers = true
|
|
|
|
case "disableStutteringCheck":
|
|
|
|
r.disableStutteringCheck = true
|
|
|
|
case "sayRepetitiveInsteadOfStutters":
|
|
|
|
r.stuttersMsg = "is repetitive"
|
|
|
|
case "checkPublicInterface":
|
|
|
|
r.checkPublicInterface = true
|
|
|
|
default:
|
|
|
|
panic(fmt.Sprintf("Unknown configuration flag %s for %s rule", flagStr, r.Name()))
|
|
|
|
}
|
2021-10-17 20:34:48 +02:00
|
|
|
}
|
|
|
|
r.configured = true
|
2021-08-26 17:36:24 +02:00
|
|
|
}
|
2022-04-10 09:06:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Apply applies the rule to given file.
|
|
|
|
func (r *ExportedRule) Apply(file *lint.File, args lint.Arguments) []lint.Failure {
|
|
|
|
r.configure(args)
|
|
|
|
|
|
|
|
var failures []lint.Failure
|
|
|
|
if file.IsTest() {
|
|
|
|
return failures
|
|
|
|
}
|
2021-08-26 17:36:24 +02:00
|
|
|
|
2018-01-22 04:48:51 +02:00
|
|
|
fileAst := file.AST
|
2022-04-10 09:06:59 +02:00
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
walker := lintExported{
|
|
|
|
file: file,
|
|
|
|
fileAst: fileAst,
|
2018-01-25 01:44:03 +02:00
|
|
|
onFailure: func(failure lint.Failure) {
|
2017-11-27 05:19:41 +02:00
|
|
|
failures = append(failures, failure)
|
|
|
|
},
|
|
|
|
genDeclMissingComments: make(map[*ast.GenDecl]bool),
|
2021-10-17 20:34:48 +02:00
|
|
|
checkPrivateReceivers: r.checkPrivateReceivers,
|
|
|
|
disableStutteringCheck: r.disableStutteringCheck,
|
2024-07-30 10:36:03 +02:00
|
|
|
checkPublicInterface: r.checkPublicInterface,
|
2021-10-17 20:34:48 +02:00
|
|
|
stuttersMsg: r.stuttersMsg,
|
2017-11-27 05:19:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
ast.Walk(&walker, fileAst)
|
|
|
|
|
|
|
|
return failures
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the rule name.
|
2022-04-10 11:55:13 +02:00
|
|
|
func (*ExportedRule) Name() string {
|
2018-01-28 03:01:18 +02:00
|
|
|
return "exported"
|
2017-11-27 05:19:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type lintExported struct {
|
2018-01-25 01:44:03 +02:00
|
|
|
file *lint.File
|
2017-11-27 05:19:41 +02:00
|
|
|
fileAst *ast.File
|
|
|
|
lastGen *ast.GenDecl
|
|
|
|
genDeclMissingComments map[*ast.GenDecl]bool
|
2018-01-25 01:44:03 +02:00
|
|
|
onFailure func(lint.Failure)
|
2021-08-26 17:36:24 +02:00
|
|
|
checkPrivateReceivers bool
|
|
|
|
disableStutteringCheck bool
|
2024-07-30 10:36:03 +02:00
|
|
|
checkPublicInterface bool
|
2021-08-26 17:36:24 +02:00
|
|
|
stuttersMsg string
|
2017-11-27 05:19:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintExported) lintFuncDoc(fn *ast.FuncDecl) {
|
|
|
|
if !ast.IsExported(fn.Name.Name) {
|
|
|
|
// func is unexported
|
|
|
|
return
|
|
|
|
}
|
|
|
|
kind := "function"
|
|
|
|
name := fn.Name.Name
|
|
|
|
if fn.Recv != nil && len(fn.Recv.List) > 0 {
|
|
|
|
// method
|
|
|
|
kind = "method"
|
2022-06-18 18:47:53 +02:00
|
|
|
recv := typeparams.ReceiverType(fn)
|
2021-10-23 13:25:41 +02:00
|
|
|
if !w.checkPrivateReceivers && !ast.IsExported(recv) {
|
2017-11-27 05:19:41 +02:00
|
|
|
// receiver is unexported
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if commonMethods[name] {
|
|
|
|
return
|
|
|
|
}
|
2018-01-24 09:01:49 +02:00
|
|
|
switch name {
|
|
|
|
case "Len", "Less", "Swap":
|
2022-04-10 09:06:59 +02:00
|
|
|
sortables := w.file.Pkg.Sortable()
|
|
|
|
if sortables[recv] {
|
2018-01-24 09:01:49 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2017-11-27 05:19:41 +02:00
|
|
|
name = recv + "." + name
|
|
|
|
}
|
|
|
|
if fn.Doc == nil {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: fn,
|
|
|
|
Confidence: 1,
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "comments",
|
|
|
|
Failure: fmt.Sprintf("exported %s %s should have comment or be unexported", kind, name),
|
2017-11-27 05:19:41 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2019-09-10 19:26:47 +02:00
|
|
|
s := normalizeText(fn.Doc.Text())
|
2017-11-27 05:19:41 +02:00
|
|
|
prefix := fn.Name.Name + " "
|
|
|
|
if !strings.HasPrefix(s, prefix) {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: fn.Doc,
|
2018-01-24 09:01:49 +02:00
|
|
|
Confidence: 0.8,
|
|
|
|
Category: "comments",
|
2017-11-27 05:19:41 +02:00
|
|
|
Failure: fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintExported) checkStutter(id *ast.Ident, thing string) {
|
2021-08-26 17:36:24 +02:00
|
|
|
if w.disableStutteringCheck {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
pkg, name := w.fileAst.Name.Name, id.Name
|
|
|
|
if !ast.IsExported(name) {
|
|
|
|
// unexported name
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// A name stutters if the package name is a strict prefix
|
|
|
|
// and the next character of the name starts a new word.
|
|
|
|
if len(name) <= len(pkg) {
|
|
|
|
// name is too short to stutter.
|
|
|
|
// This permits the name to be the same as the package name.
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !strings.EqualFold(pkg, name[:len(pkg)]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// We can assume the name is well-formed UTF-8.
|
|
|
|
// If the next rune after the package name is uppercase or an underscore
|
|
|
|
// the it's starting a new word and thus this name stutters.
|
|
|
|
rem := name[len(pkg):]
|
|
|
|
if next, _ := utf8.DecodeRuneInString(rem); next == '_' || unicode.IsUpper(next) {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: id,
|
|
|
|
Confidence: 0.8,
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "naming",
|
2021-08-26 17:36:24 +02:00
|
|
|
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),
|
2017-11-27 05:19:41 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintExported) lintTypeDoc(t *ast.TypeSpec, doc *ast.CommentGroup) {
|
|
|
|
if !ast.IsExported(t.Name.Name) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if doc == nil {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: t,
|
|
|
|
Confidence: 1,
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "comments",
|
|
|
|
Failure: fmt.Sprintf("exported type %v should have comment or be unexported", t.Name),
|
2017-11-27 05:19:41 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-10 19:26:47 +02:00
|
|
|
s := normalizeText(doc.Text())
|
2018-07-08 00:14:15 +02:00
|
|
|
articles := [...]string{"A", "An", "The", "This"}
|
2017-11-27 05:19:41 +02:00
|
|
|
for _, a := range articles {
|
2018-07-08 00:14:15 +02:00
|
|
|
if t.Name.Name == a {
|
|
|
|
continue
|
|
|
|
}
|
2017-11-27 05:19:41 +02:00
|
|
|
if strings.HasPrefix(s, a+" ") {
|
|
|
|
s = s[len(a)+1:]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2024-09-14 21:38:06 +02:00
|
|
|
// if comment starts with name of type and has some text after - it's ok
|
2024-09-29 22:50:49 +02:00
|
|
|
expectedPrefix := t.Name.Name + " "
|
|
|
|
if strings.HasPrefix(s, expectedPrefix) {
|
2024-07-30 10:36:03 +02:00
|
|
|
return
|
2017-11-27 05:19:41 +02:00
|
|
|
}
|
2024-07-30 10:36:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Node: doc,
|
|
|
|
Confidence: 1,
|
|
|
|
Category: "comments",
|
|
|
|
Failure: fmt.Sprintf(`comment on exported type %v should be of the form "%s..." (with optional leading article)`, t.Name, expectedPrefix),
|
|
|
|
})
|
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintExported) lintValueSpecDoc(vs *ast.ValueSpec, gd *ast.GenDecl, genDeclMissingComments map[*ast.GenDecl]bool) {
|
|
|
|
kind := "var"
|
|
|
|
if gd.Tok == token.CONST {
|
|
|
|
kind = "const"
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(vs.Names) > 1 {
|
|
|
|
// Check that none are exported except for the first.
|
|
|
|
for _, n := range vs.Names[1:] {
|
|
|
|
if ast.IsExported(n.Name) {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "comments",
|
2017-11-27 05:19:41 +02:00
|
|
|
Confidence: 1,
|
2018-01-24 09:01:49 +02:00
|
|
|
Failure: fmt.Sprintf("exported %s %s should have its own declaration", kind, n.Name),
|
|
|
|
Node: vs,
|
2017-11-27 05:19:41 +02:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only one name.
|
|
|
|
name := vs.Names[0].Name
|
|
|
|
if !ast.IsExported(name) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-09-29 22:50:49 +02:00
|
|
|
if vs.Doc == nil && gd.Doc == nil {
|
2017-11-27 05:19:41 +02:00
|
|
|
if genDeclMissingComments[gd] {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
block := ""
|
|
|
|
if kind == "const" && gd.Lparen.IsValid() {
|
|
|
|
block = " (or a comment on this block)"
|
|
|
|
}
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2018-01-24 09:01:49 +02:00
|
|
|
Confidence: 1,
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: vs,
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "comments",
|
2017-11-27 05:19:41 +02:00
|
|
|
Failure: fmt.Sprintf("exported %s %s should have comment%s or be unexported", kind, name, block),
|
|
|
|
})
|
|
|
|
genDeclMissingComments[gd] = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// If this GenDecl has parens and a comment, we don't check its comment form.
|
2021-10-23 13:25:41 +02:00
|
|
|
if gd.Doc != nil && gd.Lparen.IsValid() {
|
2017-11-27 05:19:41 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// The relevant text to check will be on either vs.Doc or gd.Doc.
|
|
|
|
// Use vs.Doc preferentially.
|
2022-06-28 17:14:26 +02:00
|
|
|
var doc *ast.CommentGroup
|
|
|
|
switch {
|
|
|
|
case vs.Doc != nil:
|
|
|
|
doc = vs.Doc
|
|
|
|
case vs.Comment != nil && gd.Doc == nil:
|
|
|
|
doc = vs.Comment
|
|
|
|
default:
|
2017-11-27 05:19:41 +02:00
|
|
|
doc = gd.Doc
|
|
|
|
}
|
2022-06-28 17:14:26 +02:00
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
prefix := name + " "
|
2019-09-10 19:26:47 +02:00
|
|
|
s := normalizeText(doc.Text())
|
|
|
|
if !strings.HasPrefix(s, prefix) {
|
2018-01-25 01:44:03 +02:00
|
|
|
w.onFailure(lint.Failure{
|
2018-01-24 09:01:49 +02:00
|
|
|
Confidence: 1,
|
2017-11-27 05:19:41 +02:00
|
|
|
Node: doc,
|
2018-01-24 09:01:49 +02:00
|
|
|
Category: "comments",
|
2017-11-27 05:19:41 +02:00
|
|
|
Failure: fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-10 19:26:47 +02:00
|
|
|
// normalizeText is a helper function that normalizes comment strings by:
|
|
|
|
// * removing one leading space
|
|
|
|
//
|
|
|
|
// This function is needed because ast.CommentGroup.Text() does not handle //-style and /*-style comments uniformly
|
|
|
|
func normalizeText(t string) string {
|
2024-07-30 10:36:03 +02:00
|
|
|
return strings.TrimSpace(t)
|
2019-09-10 19:26:47 +02:00
|
|
|
}
|
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
func (w *lintExported) Visit(n ast.Node) ast.Visitor {
|
|
|
|
switch v := n.(type) {
|
|
|
|
case *ast.GenDecl:
|
|
|
|
if v.Tok == token.IMPORT {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// token.CONST, token.TYPE or token.VAR
|
|
|
|
w.lastGen = v
|
|
|
|
return w
|
|
|
|
case *ast.FuncDecl:
|
|
|
|
w.lintFuncDoc(v)
|
|
|
|
if v.Recv == nil {
|
|
|
|
// Only check for stutter on functions, not methods.
|
|
|
|
// Method names are not used package-qualified.
|
|
|
|
w.checkStutter(v.Name, "func")
|
|
|
|
}
|
|
|
|
// Don't proceed inside funcs.
|
|
|
|
return nil
|
|
|
|
case *ast.TypeSpec:
|
|
|
|
// inside a GenDecl, which usually has the doc
|
|
|
|
doc := v.Doc
|
|
|
|
if doc == nil {
|
|
|
|
doc = w.lastGen.Doc
|
|
|
|
}
|
|
|
|
w.lintTypeDoc(v, doc)
|
|
|
|
w.checkStutter(v.Name, "type")
|
2024-07-30 10:36:03 +02:00
|
|
|
|
|
|
|
if w.checkPublicInterface {
|
|
|
|
if iface, ok := v.Type.(*ast.InterfaceType); ok {
|
|
|
|
if ast.IsExported(v.Name.Name) {
|
|
|
|
w.doCheckPublicInterface(v.Name.Name, iface)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-27 05:19:41 +02:00
|
|
|
return nil
|
|
|
|
case *ast.ValueSpec:
|
|
|
|
w.lintValueSpecDoc(v, w.lastGen, w.genDeclMissingComments)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return w
|
|
|
|
}
|
2024-07-30 10:36:03 +02:00
|
|
|
|
|
|
|
func (w *lintExported) doCheckPublicInterface(typeName string, iface *ast.InterfaceType) {
|
|
|
|
for _, m := range iface.Methods.List {
|
|
|
|
w.lintInterfaceMethod(typeName, m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *lintExported) lintInterfaceMethod(typeName string, m *ast.Field) {
|
|
|
|
if len(m.Names) == 0 {
|
2024-09-29 22:50:49 +02:00
|
|
|
return
|
2024-07-30 10:36:03 +02:00
|
|
|
}
|
|
|
|
if !ast.IsExported(m.Names[0].Name) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
name := m.Names[0].Name
|
|
|
|
if m.Doc == nil {
|
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Node: m,
|
|
|
|
Confidence: 1,
|
|
|
|
Category: "comments",
|
|
|
|
Failure: fmt.Sprintf("public interface method %s.%s should be commented", typeName, name),
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
s := normalizeText(m.Doc.Text())
|
|
|
|
expectedPrefix := m.Names[0].Name + " "
|
|
|
|
if !strings.HasPrefix(s, expectedPrefix) {
|
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Node: m.Doc,
|
|
|
|
Confidence: 0.8,
|
|
|
|
Category: "comments",
|
|
|
|
Failure: fmt.Sprintf(`comment on exported interface method %s.%s should be of the form "%s..."`, typeName, name, expectedPrefix),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|