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  }