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 }