s1017.go raw

   1  package s1017
   2  
   3  import (
   4  	"fmt"
   5  	"go/ast"
   6  	"go/token"
   7  	"reflect"
   8  
   9  	"honnef.co/go/tools/analysis/code"
  10  	"honnef.co/go/tools/analysis/facts/generated"
  11  	"honnef.co/go/tools/analysis/lint"
  12  	"honnef.co/go/tools/analysis/report"
  13  	"honnef.co/go/tools/go/ast/astutil"
  14  	"honnef.co/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.RawDocumentation{
  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  	seen := make(map[ast.Node]struct{})
  77  	fn := func(node ast.Node) {
  78  		var pkg string
  79  		var fun string
  80  
  81  		ifstmt := node.(*ast.IfStmt)
  82  		if ifstmt.Init != nil {
  83  			return
  84  		}
  85  		if ifstmt.Else != nil {
  86  			seen[ifstmt.Else] = struct{}{}
  87  			return
  88  		}
  89  		if _, ok := seen[ifstmt]; ok {
  90  			return
  91  		}
  92  		if len(ifstmt.Body.List) != 1 {
  93  			return
  94  		}
  95  		condCall, ok := ifstmt.Cond.(*ast.CallExpr)
  96  		if !ok {
  97  			return
  98  		}
  99  
 100  		condCallName := code.CallName(pass, condCall)
 101  		switch condCallName {
 102  		case "strings.HasPrefix":
 103  			pkg = "strings"
 104  			fun = "HasPrefix"
 105  		case "strings.HasSuffix":
 106  			pkg = "strings"
 107  			fun = "HasSuffix"
 108  		case "strings.Contains":
 109  			pkg = "strings"
 110  			fun = "Contains"
 111  		case "bytes.HasPrefix":
 112  			pkg = "bytes"
 113  			fun = "HasPrefix"
 114  		case "bytes.HasSuffix":
 115  			pkg = "bytes"
 116  			fun = "HasSuffix"
 117  		case "bytes.Contains":
 118  			pkg = "bytes"
 119  			fun = "Contains"
 120  		default:
 121  			return
 122  		}
 123  
 124  		assign, ok := ifstmt.Body.List[0].(*ast.AssignStmt)
 125  		if !ok {
 126  			return
 127  		}
 128  		if assign.Tok != token.ASSIGN {
 129  			return
 130  		}
 131  		if len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
 132  			return
 133  		}
 134  		if !sameNonDynamic(condCall.Args[0], assign.Lhs[0]) {
 135  			return
 136  		}
 137  
 138  		switch rhs := assign.Rhs[0].(type) {
 139  		case *ast.CallExpr:
 140  			if len(rhs.Args) < 2 || !sameNonDynamic(condCall.Args[0], rhs.Args[0]) || !sameNonDynamic(condCall.Args[1], rhs.Args[1]) {
 141  				return
 142  			}
 143  
 144  			rhsName := code.CallName(pass, rhs)
 145  			if condCallName == "strings.HasPrefix" && rhsName == "strings.TrimPrefix" ||
 146  				condCallName == "strings.HasSuffix" && rhsName == "strings.TrimSuffix" ||
 147  				condCallName == "strings.Contains" && rhsName == "strings.Replace" ||
 148  				condCallName == "bytes.HasPrefix" && rhsName == "bytes.TrimPrefix" ||
 149  				condCallName == "bytes.HasSuffix" && rhsName == "bytes.TrimSuffix" ||
 150  				condCallName == "bytes.Contains" && rhsName == "bytes.Replace" {
 151  				report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s", rhsName), report.FilterGenerated())
 152  			}
 153  		case *ast.SliceExpr:
 154  			slice := rhs
 155  			if !ok {
 156  				return
 157  			}
 158  			if slice.Slice3 {
 159  				return
 160  			}
 161  			if !sameNonDynamic(slice.X, condCall.Args[0]) {
 162  				return
 163  			}
 164  
 165  			validateOffset := func(off ast.Expr) bool {
 166  				switch off := off.(type) {
 167  				case *ast.CallExpr:
 168  					return isLenOnIdent(off, condCall.Args[1])
 169  				case *ast.BasicLit:
 170  					if pkg != "strings" {
 171  						return false
 172  					}
 173  					if _, ok := condCall.Args[1].(*ast.BasicLit); !ok {
 174  						// Only allow manual slicing with an integer
 175  						// literal if the second argument to HasPrefix
 176  						// was a string literal.
 177  						return false
 178  					}
 179  					s, ok1 := code.ExprToString(pass, condCall.Args[1])
 180  					n, ok2 := code.ExprToInt(pass, off)
 181  					if !ok1 || !ok2 || n != int64(len(s)) {
 182  						return false
 183  					}
 184  					return true
 185  				default:
 186  					return false
 187  				}
 188  			}
 189  
 190  			switch fun {
 191  			case "HasPrefix":
 192  				// TODO(dh) We could detect a High that is len(s), but another
 193  				// rule will already flag that, anyway.
 194  				if slice.High != nil {
 195  					return
 196  				}
 197  				if !validateOffset(slice.Low) {
 198  					return
 199  				}
 200  			case "HasSuffix":
 201  				if slice.Low != nil {
 202  					n, ok := code.ExprToInt(pass, slice.Low)
 203  					if !ok || n != 0 {
 204  						return
 205  					}
 206  				}
 207  				switch index := slice.High.(type) {
 208  				case *ast.BinaryExpr:
 209  					if index.Op != token.SUB {
 210  						return
 211  					}
 212  					if !isLenOnIdent(index.X, condCall.Args[0]) {
 213  						return
 214  					}
 215  					if !validateOffset(index.Y) {
 216  						return
 217  					}
 218  				default:
 219  					return
 220  				}
 221  			default:
 222  				return
 223  			}
 224  
 225  			var replacement string
 226  			switch fun {
 227  			case "HasPrefix":
 228  				replacement = "TrimPrefix"
 229  			case "HasSuffix":
 230  				replacement = "TrimSuffix"
 231  			}
 232  			report.Report(pass, ifstmt, fmt.Sprintf("should replace this if statement with an unconditional %s.%s", pkg, replacement),
 233  				report.ShortRange(),
 234  				report.FilterGenerated())
 235  		}
 236  	}
 237  	code.Preorder(pass, fn, (*ast.IfStmt)(nil))
 238  	return nil, nil
 239  }
 240