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