st1005.go raw

   1  package st1005
   2  
   3  import (
   4  	"go/constant"
   5  	"strings"
   6  	"unicode"
   7  	"unicode/utf8"
   8  
   9  	"honnef.co/go/tools/analysis/code"
  10  	"honnef.co/go/tools/analysis/lint"
  11  	"honnef.co/go/tools/analysis/report"
  12  	"honnef.co/go/tools/go/ir"
  13  	"honnef.co/go/tools/go/ir/irutil"
  14  	"honnef.co/go/tools/internal/passes/buildir"
  15  
  16  	"golang.org/x/tools/go/analysis"
  17  )
  18  
  19  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  20  	Analyzer: &analysis.Analyzer{
  21  		Name:     "ST1005",
  22  		Run:      run,
  23  		Requires: []*analysis.Analyzer{buildir.Analyzer},
  24  	},
  25  	Doc: &lint.RawDocumentation{
  26  		Title: `Incorrectly formatted error string`,
  27  		Text: `Error strings follow a set of guidelines to ensure uniformity and good
  28  composability.
  29  
  30  Quoting Go Code Review Comments:
  31  
  32  > Error strings should not be capitalized (unless beginning with
  33  > proper nouns or acronyms) or end with punctuation, since they are
  34  > usually printed following other context. That is, use
  35  > \'fmt.Errorf("something bad")\' not \'fmt.Errorf("Something bad")\', so
  36  > that \'log.Printf("Reading %s: %v", filename, err)\' formats without a
  37  > spurious capital letter mid-message.`,
  38  		Since:   "2019.1",
  39  		MergeIf: lint.MergeIfAny,
  40  	},
  41  })
  42  
  43  var Analyzer = SCAnalyzer.Analyzer
  44  
  45  func run(pass *analysis.Pass) (interface{}, error) {
  46  	objNames := map[*ir.Package]map[string]bool{}
  47  	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
  48  	objNames[irpkg] = map[string]bool{}
  49  	for _, m := range irpkg.Members {
  50  		if typ, ok := m.(*ir.Type); ok {
  51  			objNames[irpkg][typ.Name()] = true
  52  		}
  53  	}
  54  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
  55  		objNames[fn.Package()][fn.Name()] = true
  56  	}
  57  
  58  	for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
  59  		if code.IsInTest(pass, fn) {
  60  			// We don't care about malformed error messages in tests;
  61  			// they're usually for direct human consumption, not part
  62  			// of an API
  63  			continue
  64  		}
  65  		for _, block := range fn.Blocks {
  66  		instrLoop:
  67  			for _, ins := range block.Instrs {
  68  				call, ok := ins.(*ir.Call)
  69  				if !ok {
  70  					continue
  71  				}
  72  				if !irutil.IsCallToAny(call.Common(), "errors.New", "fmt.Errorf") {
  73  					continue
  74  				}
  75  
  76  				k, ok := call.Common().Args[0].(*ir.Const)
  77  				if !ok {
  78  					continue
  79  				}
  80  
  81  				s := constant.StringVal(k.Value)
  82  				if len(s) == 0 {
  83  					continue
  84  				}
  85  				switch s[len(s)-1] {
  86  				case '.', ':', '!', '\n':
  87  					report.Report(pass, call, "error strings should not end with punctuation or newlines")
  88  				}
  89  				idx := strings.IndexByte(s, ' ')
  90  				if idx == -1 {
  91  					// single word error message, probably not a real
  92  					// error but something used in tests or during
  93  					// debugging
  94  					continue
  95  				}
  96  				word := s[:idx]
  97  				first, n := utf8.DecodeRuneInString(word)
  98  				if !unicode.IsUpper(first) {
  99  					continue
 100  				}
 101  				for _, c := range word[n:] {
 102  					if unicode.IsUpper(c) || unicode.IsDigit(c) {
 103  						// Word is probably an initialism or multi-word function name. Digits cover elliptic curves like
 104  						// P384.
 105  						continue instrLoop
 106  					}
 107  				}
 108  
 109  				if strings.ContainsRune(word, '(') {
 110  					// Might be a function call
 111  					continue instrLoop
 112  				}
 113  				word = strings.TrimRightFunc(word, func(r rune) bool { return unicode.IsPunct(r) })
 114  				if objNames[fn.Package()][word] {
 115  					// Word is probably the name of a function or type in this package
 116  					continue
 117  				}
 118  				// First word in error starts with a capital
 119  				// letter, and the word doesn't contain any other
 120  				// capitals, making it unlikely to be an
 121  				// initialism or multi-word function name.
 122  				//
 123  				// It could still be a proper noun, though.
 124  
 125  				report.Report(pass, call, "error strings should not be capitalized")
 126  			}
 127  		}
 128  	}
 129  	return nil, nil
 130  }
 131