src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

s1017.go (5920B)


      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) (any, 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 }