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  }