github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/stylecheck/st1005/st1005.go (about)

     1  package st1005
     2  
     3  import (
     4  	"go/constant"
     5  	"strings"
     6  	"unicode"
     7  	"unicode/utf8"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/lint"
    11  	"github.com/amarpal/go-tools/analysis/report"
    12  	"github.com/amarpal/go-tools/go/ir"
    13  	"github.com/amarpal/go-tools/go/ir/irutil"
    14  	"github.com/amarpal/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.Documentation{
    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  }