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

     1  package s1017
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"reflect"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/facts/generated"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/go-tools/analysis/report"
    13  	"github.com/amarpal/go-tools/go/ast/astutil"
    14  	"github.com/amarpal/go-tools/knowledge"
    15  
    16  	"golang.org/x/tools/go/analysis"
    17  	"golang.org/x/tools/go/analysis/passes/inspect"
    18  )
    19  
    20  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    21  	Analyzer: &analysis.Analyzer{
    22  		Name:     "S1017",
    23  		Run:      run,
    24  		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
    25  	},
    26  	Doc: &lint.Documentation{
    27  		Title: `Replace manual trimming with \'strings.TrimPrefix\'`,
    28  		Text: `Instead of using \'strings.HasPrefix\' and manual slicing, use the
    29  \'strings.TrimPrefix\' function. If the string doesn't start with the
    30  prefix, the original string will be returned. Using \'strings.TrimPrefix\'
    31  reduces complexity, and avoids common bugs, such as off-by-one
    32  mistakes.`,
    33  		Before: `
    34  if strings.HasPrefix(str, prefix) {
    35      str = str[len(prefix):]
    36  }`,
    37  		After:   `str = strings.TrimPrefix(str, prefix)`,
    38  		Since:   "2017.1",
    39  		MergeIf: lint.MergeIfAny,
    40  	},
    41  })
    42  
    43  var Analyzer = SCAnalyzer.Analyzer
    44  
    45  func run(pass *analysis.Pass) (interface{}, error) {
    46  	sameNonDynamic := func(node1, node2 ast.Node) bool {
    47  		if reflect.TypeOf(node1) != reflect.TypeOf(node2) {
    48  			return false
    49  		}
    50  
    51  		switch node1 := node1.(type) {
    52  		case *ast.Ident:
    53  			return pass.TypesInfo.ObjectOf(node1) == pass.TypesInfo.ObjectOf(node2.(*ast.Ident))
    54  		case *ast.SelectorExpr, *ast.IndexExpr:
    55  			return astutil.Equal(node1, node2)
    56  		case *ast.BasicLit:
    57  			return astutil.Equal(node1, node2)
    58  		}
    59  		return false
    60  	}
    61  
    62  	isLenOnIdent := func(fn ast.Expr, ident ast.Expr) bool {
    63  		call, ok := fn.(*ast.CallExpr)
    64  		if !ok {
    65  			return false
    66  		}
    67  		if !code.IsCallTo(pass, call, "len") {
    68  			return false
    69  		}
    70  		if len(call.Args) != 1 {
    71  			return false
    72  		}
    73  		return sameNonDynamic(call.Args[knowledge.Arg("len.v")], ident)
    74  	}
    75  
    76  	fn := func(node ast.Node) {
    77  		var pkg string
    78  		var fun string
    79  
    80  		ifstmt := node.(*ast.IfStmt)
    81  		if ifstmt.Init != nil {
    82  			return
    83  		}
    84  		if ifstmt.Else != nil {
    85  			return
    86  		}
    87  		if len(ifstmt.Body.List) != 1 {
    88  			return
    89  		}
    90  		condCall, ok := ifstmt.Cond.(*ast.CallExpr)
    91  		if !ok {
    92  			return
    93  		}
    94  
    95  		condCallName := code.CallName(pass, condCall)
    96  		switch condCallName {
    97  		case "strings.HasPrefix":
    98  			pkg = "strings"
    99  			fun = "HasPrefix"
   100  		case "strings.HasSuffix":
   101  			pkg = "strings"
   102  			fun = "HasSuffix"
   103  		case "strings.Contains":
   104  			pkg = "strings"
   105  			fun = "Contains"
   106  		case "bytes.HasPrefix":
   107  			pkg = "bytes"
   108  			fun = "HasPrefix"
   109  		case "bytes.HasSuffix":
   110  			pkg = "bytes"
   111  			fun = "HasSuffix"
   112  		case "bytes.Contains":
   113  			pkg = "bytes"
   114  			fun = "Contains"
   115  		default:
   116  			return
   117  		}
   118  
   119  		assign, ok := ifstmt.Body.List[0].(*ast.AssignStmt)
   120  		if !ok {
   121  			return
   122  		}
   123  		if assign.Tok != token.ASSIGN {
   124  			return
   125  		}
   126  		if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
   127  			return
   128  		}
   129  		if !sameNonDynamic(condCall.Args[0], assign.Lhs[0]) {
   130  			return
   131  		}
   132  
   133  		switch rhs := assign.Rhs[0].(type) {
   134  		case *ast.CallExpr:
   135  			if len(rhs.Args) < 2 || !sameNonDynamic(condCall.Args[0], rhs.Args[0]) || !sameNonDynamic(condCall.Args[1], rhs.Args[1]) {
   136  				return
   137  			}
   138  
   139  			rhsName := code.CallName(pass, rhs)
   140  			if condCallName == "strings.HasPrefix" && rhsName == "strings.TrimPrefix" ||
   141  				condCallName == "strings.HasSuffix" && rhsName == "strings.TrimSuffix" ||
   142  				condCallName == "strings.Contains" && rhsName == "strings.Replace" ||
   143  				condCallName == "bytes.HasPrefix" && rhsName == "bytes.TrimPrefix" ||
   144  				condCallName == "bytes.HasSuffix" && rhsName == "bytes.TrimSuffix" ||
   145  				condCallName == "bytes.Contains" && rhsName == "bytes.Replace" {
   146  				report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s", rhsName), report.FilterGenerated())
   147  			}
   148  		case *ast.SliceExpr:
   149  			slice := rhs
   150  			if !ok {
   151  				return
   152  			}
   153  			if slice.Slice3 {
   154  				return
   155  			}
   156  			if !sameNonDynamic(slice.X, condCall.Args[0]) {
   157  				return
   158  			}
   159  
   160  			validateOffset := func(off ast.Expr) bool {
   161  				switch off := off.(type) {
   162  				case *ast.CallExpr:
   163  					return isLenOnIdent(off, condCall.Args[1])
   164  				case *ast.BasicLit:
   165  					if pkg != "strings" {
   166  						return false
   167  					}
   168  					if _, ok := condCall.Args[1].(*ast.BasicLit); !ok {
   169  						// Only allow manual slicing with an integer
   170  						// literal if the second argument to HasPrefix
   171  						// was a string literal.
   172  						return false
   173  					}
   174  					s, ok1 := code.ExprToString(pass, condCall.Args[1])
   175  					n, ok2 := code.ExprToInt(pass, off)
   176  					if !ok1 || !ok2 || n != int64(len(s)) {
   177  						return false
   178  					}
   179  					return true
   180  				default:
   181  					return false
   182  				}
   183  			}
   184  
   185  			switch fun {
   186  			case "HasPrefix":
   187  				// TODO(dh) We could detect a High that is len(s), but another
   188  				// rule will already flag that, anyway.
   189  				if slice.High != nil {
   190  					return
   191  				}
   192  				if !validateOffset(slice.Low) {
   193  					return
   194  				}
   195  			case "HasSuffix":
   196  				if slice.Low != nil {
   197  					n, ok := code.ExprToInt(pass, slice.Low)
   198  					if !ok || n != 0 {
   199  						return
   200  					}
   201  				}
   202  				switch index := slice.High.(type) {
   203  				case *ast.BinaryExpr:
   204  					if index.Op != token.SUB {
   205  						return
   206  					}
   207  					if !isLenOnIdent(index.X, condCall.Args[0]) {
   208  						return
   209  					}
   210  					if !validateOffset(index.Y) {
   211  						return
   212  					}
   213  				default:
   214  					return
   215  				}
   216  			default:
   217  				return
   218  			}
   219  
   220  			var replacement string
   221  			switch fun {
   222  			case "HasPrefix":
   223  				replacement = "TrimPrefix"
   224  			case "HasSuffix":
   225  				replacement = "TrimSuffix"
   226  			}
   227  			report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s.%s", pkg, replacement),
   228  				report.ShortRange(),
   229  				report.FilterGenerated())
   230  		}
   231  	}
   232  	code.Preorder(pass, fn, (*ast.IfStmt)(nil))
   233  	return nil, nil
   234  }