github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/error-strings.go (about)

     1  package rule
     2  
     3  import (
     4  	"go/ast"
     5  	"go/token"
     6  	"strconv"
     7  	"unicode"
     8  	"unicode/utf8"
     9  
    10  	"github.com/songshiyun/revive/lint"
    11  )
    12  
    13  // ErrorStringsRule lints given else constructs.
    14  type ErrorStringsRule struct{}
    15  
    16  // Apply applies the rule to given file.
    17  func (r *ErrorStringsRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
    18  	var failures []lint.Failure
    19  
    20  	errorFunctions := map[string]map[string]struct{}{
    21  		"fmt": {
    22  			"Errorf": {},
    23  		},
    24  		"errors": {
    25  			"Errorf":       {},
    26  			"WithMessage":  {},
    27  			"Wrap":         {},
    28  			"New":          {},
    29  			"WithMessagef": {},
    30  			"Wrapf":        {},
    31  		},
    32  	}
    33  
    34  	fileAst := file.AST
    35  	walker := lintErrorStrings{
    36  		file:           file,
    37  		fileAst:        fileAst,
    38  		errorFunctions: errorFunctions,
    39  		onFailure: func(failure lint.Failure) {
    40  			failures = append(failures, failure)
    41  		},
    42  	}
    43  
    44  	ast.Walk(walker, fileAst)
    45  
    46  	return failures
    47  }
    48  
    49  // Name returns the rule name.
    50  func (r *ErrorStringsRule) Name() string {
    51  	return "error-strings"
    52  }
    53  
    54  type lintErrorStrings struct {
    55  	file           *lint.File
    56  	fileAst        *ast.File
    57  	errorFunctions map[string]map[string]struct{}
    58  	onFailure      func(lint.Failure)
    59  }
    60  
    61  // Visit browses the AST
    62  func (w lintErrorStrings) Visit(n ast.Node) ast.Visitor {
    63  	ce, ok := n.(*ast.CallExpr)
    64  	if !ok {
    65  		return w
    66  	}
    67  
    68  	if len(ce.Args) < 1 {
    69  		return w
    70  	}
    71  
    72  	// expression matches the known pkg.function
    73  	ok = w.match(ce)
    74  	if !ok {
    75  		return w
    76  	}
    77  
    78  	str, ok := w.getMessage(ce)
    79  	if !ok {
    80  		return w
    81  	}
    82  	s, _ := strconv.Unquote(str.Value) // can assume well-formed Go
    83  	if s == "" {
    84  		return w
    85  	}
    86  	clean, conf := lintErrorString(s)
    87  	if clean {
    88  		return w
    89  	}
    90  	w.onFailure(lint.Failure{
    91  		Node:       str,
    92  		Confidence: conf,
    93  		Category:   "errors",
    94  		Failure:    "error strings should not be capitalized or end with punctuation or a newline",
    95  	})
    96  	return w
    97  }
    98  
    99  // match returns true if the expression corresponds to the known pkg.function
   100  // i.e.: errors.Wrap
   101  func (w lintErrorStrings) match(expr *ast.CallExpr) bool {
   102  	sel, ok := expr.Fun.(*ast.SelectorExpr)
   103  	if !ok {
   104  		return false
   105  	}
   106  	// retrieve the package
   107  	id, ok := sel.X.(*ast.Ident)
   108  	if !ok {
   109  		return false
   110  	}
   111  	functions, ok := w.errorFunctions[id.Name]
   112  	if !ok {
   113  		return false
   114  	}
   115  	// retrieve the function
   116  	_, ok = functions[sel.Sel.Name]
   117  	return ok
   118  }
   119  
   120  // getMessage returns the message depending on its position
   121  // returns false if the cast is unsuccessful
   122  func (w lintErrorStrings) getMessage(expr *ast.CallExpr) (s *ast.BasicLit, success bool) {
   123  	str, ok := w.checkArg(expr, 0)
   124  	if ok {
   125  		return str, true
   126  	}
   127  	if len(expr.Args) < 2 {
   128  		return s, false
   129  	}
   130  	str, ok = w.checkArg(expr, 1)
   131  	if !ok {
   132  		return s, false
   133  	}
   134  	return str, true
   135  }
   136  
   137  func (lintErrorStrings) checkArg(expr *ast.CallExpr, arg int) (s *ast.BasicLit, success bool) {
   138  	str, ok := expr.Args[arg].(*ast.BasicLit)
   139  	if !ok {
   140  		return s, false
   141  	}
   142  	if str.Kind != token.STRING {
   143  		return s, false
   144  	}
   145  	return str, true
   146  }
   147  
   148  func lintErrorString(s string) (isClean bool, conf float64) {
   149  	const basicConfidence = 0.8
   150  	const capConfidence = basicConfidence - 0.2
   151  	first, firstN := utf8.DecodeRuneInString(s)
   152  	last, _ := utf8.DecodeLastRuneInString(s)
   153  	if last == '.' || last == ':' || last == '!' || last == '\n' {
   154  		return false, basicConfidence
   155  	}
   156  	if unicode.IsUpper(first) {
   157  		// People use proper nouns and exported Go identifiers in error strings,
   158  		// so decrease the confidence of warnings for capitalization.
   159  		if len(s) <= firstN {
   160  			return false, capConfidence
   161  		}
   162  		// Flag strings starting with something that doesn't look like an initialism.
   163  		if second, _ := utf8.DecodeRuneInString(s[firstN:]); !unicode.IsUpper(second) {
   164  			return false, capConfidence
   165  		}
   166  	}
   167  	return true, 0
   168  }