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

     1  package sa6005
     2  
     3  import (
     4  	"go/ast"
     5  	"go/token"
     6  
     7  	"github.com/amarpal/go-tools/analysis/code"
     8  	"github.com/amarpal/go-tools/analysis/edit"
     9  	"github.com/amarpal/go-tools/analysis/lint"
    10  	"github.com/amarpal/go-tools/analysis/report"
    11  	"github.com/amarpal/go-tools/pattern"
    12  
    13  	"golang.org/x/tools/go/analysis"
    14  	"golang.org/x/tools/go/analysis/passes/inspect"
    15  )
    16  
    17  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    18  	Analyzer: &analysis.Analyzer{
    19  		Name:     "SA6005",
    20  		Run:      run,
    21  		Requires: []*analysis.Analyzer{inspect.Analyzer},
    22  	},
    23  	Doc: &lint.Documentation{
    24  		Title: `Inefficient string comparison with \'strings.ToLower\' or \'strings.ToUpper\'`,
    25  		Text: `Converting two strings to the same case and comparing them like so
    26  
    27      if strings.ToLower(s1) == strings.ToLower(s2) {
    28          ...
    29      }
    30  
    31  is significantly more expensive than comparing them with
    32  \'strings.EqualFold(s1, s2)\'. This is due to memory usage as well as
    33  computational complexity.
    34  
    35  \'strings.ToLower\' will have to allocate memory for the new strings, as
    36  well as convert both strings fully, even if they differ on the very
    37  first byte. strings.EqualFold, on the other hand, compares the strings
    38  one character at a time. It doesn't need to create two intermediate
    39  strings and can return as soon as the first non-matching character has
    40  been found.
    41  
    42  For a more in-depth explanation of this issue, see
    43  https://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/`,
    44  		Since:    "2019.2",
    45  		Severity: lint.SeverityWarning,
    46  		MergeIf:  lint.MergeIfAny,
    47  	},
    48  })
    49  
    50  var Analyzer = SCAnalyzer.Analyzer
    51  
    52  var (
    53  	checkToLowerToUpperComparisonQ = pattern.MustParse(`
    54  	(BinaryExpr
    55  		(CallExpr fun@(Symbol (Or "strings.ToLower" "strings.ToUpper")) [a])
    56   		tok@(Or "==" "!=")
    57   		(CallExpr fun [b]))`)
    58  	checkToLowerToUpperComparisonR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "strings") (Ident "EqualFold")) [a b])`)
    59  )
    60  
    61  func run(pass *analysis.Pass) (interface{}, error) {
    62  	fn := func(node ast.Node) {
    63  		m, ok := code.Match(pass, checkToLowerToUpperComparisonQ, node)
    64  		if !ok {
    65  			return
    66  		}
    67  		rn := pattern.NodeToAST(checkToLowerToUpperComparisonR.Root, m.State).(ast.Expr)
    68  		if m.State["tok"].(token.Token) == token.NEQ {
    69  			rn = &ast.UnaryExpr{
    70  				Op: token.NOT,
    71  				X:  rn,
    72  			}
    73  		}
    74  
    75  		report.Report(pass, node, "should use strings.EqualFold instead", report.Fixes(edit.Fix("replace with strings.EqualFold", edit.ReplaceWithNode(pass.Fset, node, rn))))
    76  	}
    77  
    78  	code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
    79  	return nil, nil
    80  }