]>
Commit | Line | Data |
---|---|---|
17df50a5 | 1 | use clippy_utils::consts::{constant, Constant}; |
cdc7bbd5 XL |
2 | use clippy_utils::diagnostics::{multispan_sugg, span_lint_and_then}; |
3 | use clippy_utils::source::snippet; | |
4 | use clippy_utils::usage::mutated_variables; | |
5 | use clippy_utils::{eq_expr_value, higher, match_def_path, meets_msrv, msrvs, paths}; | |
f20569fa XL |
6 | use if_chain::if_chain; |
7 | use rustc_ast::ast::LitKind; | |
8 | use rustc_hir::def::Res; | |
9 | use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor}; | |
10 | use rustc_hir::BinOpKind; | |
11 | use rustc_hir::{BorrowKind, Expr, ExprKind}; | |
12 | use rustc_lint::{LateContext, LateLintPass, LintContext}; | |
13 | use rustc_middle::hir::map::Map; | |
14 | use rustc_middle::ty; | |
15 | use rustc_semver::RustcVersion; | |
16 | use rustc_session::{declare_tool_lint, impl_lint_pass}; | |
17 | use rustc_span::source_map::Spanned; | |
18 | use rustc_span::Span; | |
19 | ||
f20569fa | 20 | declare_clippy_lint! { |
94222f64 | 21 | /// ### What it does |
f20569fa XL |
22 | /// Suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing using |
23 | /// the pattern's length. | |
24 | /// | |
94222f64 | 25 | /// ### Why is this bad? |
f20569fa XL |
26 | /// Using `str:strip_{prefix,suffix}` is safer and may have better performance as there is no |
27 | /// slicing which may panic and the compiler does not need to insert this panic code. It is | |
28 | /// also sometimes more readable as it removes the need for duplicating or storing the pattern | |
29 | /// used by `str::{starts,ends}_with` and in the slicing. | |
30 | /// | |
94222f64 | 31 | /// ### Example |
f20569fa XL |
32 | /// ```rust |
33 | /// let s = "hello, world!"; | |
34 | /// if s.starts_with("hello, ") { | |
35 | /// assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!"); | |
36 | /// } | |
37 | /// ``` | |
38 | /// Use instead: | |
39 | /// ```rust | |
40 | /// let s = "hello, world!"; | |
41 | /// if let Some(end) = s.strip_prefix("hello, ") { | |
42 | /// assert_eq!(end.to_uppercase(), "WORLD!"); | |
43 | /// } | |
44 | /// ``` | |
45 | pub MANUAL_STRIP, | |
46 | complexity, | |
47 | "suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing" | |
48 | } | |
49 | ||
50 | pub struct ManualStrip { | |
51 | msrv: Option<RustcVersion>, | |
52 | } | |
53 | ||
54 | impl ManualStrip { | |
55 | #[must_use] | |
56 | pub fn new(msrv: Option<RustcVersion>) -> Self { | |
57 | Self { msrv } | |
58 | } | |
59 | } | |
60 | ||
61 | impl_lint_pass!(ManualStrip => [MANUAL_STRIP]); | |
62 | ||
63 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] | |
64 | enum StripKind { | |
65 | Prefix, | |
66 | Suffix, | |
67 | } | |
68 | ||
69 | impl<'tcx> LateLintPass<'tcx> for ManualStrip { | |
70 | fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { | |
cdc7bbd5 | 71 | if !meets_msrv(self.msrv.as_ref(), &msrvs::STR_STRIP_PREFIX) { |
f20569fa XL |
72 | return; |
73 | } | |
74 | ||
75 | if_chain! { | |
94222f64 | 76 | if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr); |
f20569fa XL |
77 | if let ExprKind::MethodCall(_, _, [target_arg, pattern], _) = cond.kind; |
78 | if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(cond.hir_id); | |
79 | if let ExprKind::Path(target_path) = &target_arg.kind; | |
80 | then { | |
81 | let strip_kind = if match_def_path(cx, method_def_id, &paths::STR_STARTS_WITH) { | |
82 | StripKind::Prefix | |
83 | } else if match_def_path(cx, method_def_id, &paths::STR_ENDS_WITH) { | |
84 | StripKind::Suffix | |
85 | } else { | |
86 | return; | |
87 | }; | |
cdc7bbd5 | 88 | let target_res = cx.qpath_res(target_path, target_arg.hir_id); |
f20569fa XL |
89 | if target_res == Res::Err { |
90 | return; | |
91 | }; | |
92 | ||
93 | if_chain! { | |
94 | if let Res::Local(hir_id) = target_res; | |
95 | if let Some(used_mutably) = mutated_variables(then, cx); | |
96 | if used_mutably.contains(&hir_id); | |
97 | then { | |
98 | return; | |
99 | } | |
100 | } | |
101 | ||
102 | let strippings = find_stripping(cx, strip_kind, target_res, pattern, then); | |
103 | if !strippings.is_empty() { | |
104 | ||
105 | let kind_word = match strip_kind { | |
106 | StripKind::Prefix => "prefix", | |
107 | StripKind::Suffix => "suffix", | |
108 | }; | |
109 | ||
110 | let test_span = expr.span.until(then.span); | |
111 | span_lint_and_then(cx, MANUAL_STRIP, strippings[0], &format!("stripping a {} manually", kind_word), |diag| { | |
112 | diag.span_note(test_span, &format!("the {} was tested here", kind_word)); | |
113 | multispan_sugg( | |
114 | diag, | |
115 | &format!("try using the `strip_{}` method", kind_word), | |
116 | vec![(test_span, | |
117 | format!("if let Some(<stripped>) = {}.strip_{}({}) ", | |
118 | snippet(cx, target_arg.span, ".."), | |
119 | kind_word, | |
120 | snippet(cx, pattern.span, "..")))] | |
121 | .into_iter().chain(strippings.into_iter().map(|span| (span, "<stripped>".into()))), | |
17df50a5 | 122 | ); |
f20569fa XL |
123 | }); |
124 | } | |
125 | } | |
126 | } | |
127 | } | |
128 | ||
129 | extract_msrv_attr!(LateContext); | |
130 | } | |
131 | ||
132 | // Returns `Some(arg)` if `expr` matches `arg.len()` and `None` otherwise. | |
133 | fn len_arg<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> { | |
134 | if_chain! { | |
135 | if let ExprKind::MethodCall(_, _, [arg], _) = expr.kind; | |
136 | if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id); | |
137 | if match_def_path(cx, method_def_id, &paths::STR_LEN); | |
138 | then { | |
139 | Some(arg) | |
140 | } else { | |
141 | None | |
142 | } | |
143 | } | |
144 | } | |
145 | ||
146 | // Returns the length of the `expr` if it's a constant string or char. | |
147 | fn constant_length(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> { | |
148 | let (value, _) = constant(cx, cx.typeck_results(), expr)?; | |
149 | match value { | |
150 | Constant::Str(value) => Some(value.len() as u128), | |
151 | Constant::Char(value) => Some(value.len_utf8() as u128), | |
152 | _ => None, | |
153 | } | |
154 | } | |
155 | ||
156 | // Tests if `expr` equals the length of the pattern. | |
157 | fn eq_pattern_length<'tcx>(cx: &LateContext<'tcx>, pattern: &Expr<'_>, expr: &'tcx Expr<'_>) -> bool { | |
158 | if let ExprKind::Lit(Spanned { | |
159 | node: LitKind::Int(n, _), | |
160 | .. | |
161 | }) = expr.kind | |
162 | { | |
163 | constant_length(cx, pattern).map_or(false, |length| length == n) | |
164 | } else { | |
165 | len_arg(cx, expr).map_or(false, |arg| eq_expr_value(cx, pattern, arg)) | |
166 | } | |
167 | } | |
168 | ||
169 | // Tests if `expr` is a `&str`. | |
170 | fn is_ref_str(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool { | |
cdc7bbd5 | 171 | match cx.typeck_results().expr_ty_adjusted(expr).kind() { |
f20569fa XL |
172 | ty::Ref(_, ty, _) => ty.is_str(), |
173 | _ => false, | |
174 | } | |
175 | } | |
176 | ||
177 | // Removes the outer `AddrOf` expression if needed. | |
178 | fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> { | |
179 | if let ExprKind::AddrOf(BorrowKind::Ref, _, unref) = &expr.kind { | |
180 | unref | |
181 | } else { | |
182 | expr | |
183 | } | |
184 | } | |
185 | ||
186 | // Find expressions where `target` is stripped using the length of `pattern`. | |
187 | // We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}` | |
188 | // method. | |
189 | fn find_stripping<'tcx>( | |
190 | cx: &LateContext<'tcx>, | |
191 | strip_kind: StripKind, | |
192 | target: Res, | |
193 | pattern: &'tcx Expr<'_>, | |
194 | expr: &'tcx Expr<'_>, | |
195 | ) -> Vec<Span> { | |
196 | struct StrippingFinder<'a, 'tcx> { | |
197 | cx: &'a LateContext<'tcx>, | |
198 | strip_kind: StripKind, | |
199 | target: Res, | |
200 | pattern: &'tcx Expr<'tcx>, | |
201 | results: Vec<Span>, | |
202 | } | |
203 | ||
204 | impl<'a, 'tcx> Visitor<'tcx> for StrippingFinder<'a, 'tcx> { | |
205 | type Map = Map<'tcx>; | |
206 | fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> { | |
207 | NestedVisitorMap::None | |
208 | } | |
209 | ||
210 | fn visit_expr(&mut self, ex: &'tcx Expr<'_>) { | |
211 | if_chain! { | |
212 | if is_ref_str(self.cx, ex); | |
213 | let unref = peel_ref(ex); | |
214 | if let ExprKind::Index(indexed, index) = &unref.kind; | |
94222f64 | 215 | if let Some(higher::Range { start, end, .. }) = higher::Range::hir(index); |
f20569fa XL |
216 | if let ExprKind::Path(path) = &indexed.kind; |
217 | if self.cx.qpath_res(path, ex.hir_id) == self.target; | |
218 | then { | |
219 | match (self.strip_kind, start, end) { | |
220 | (StripKind::Prefix, Some(start), None) => { | |
221 | if eq_pattern_length(self.cx, self.pattern, start) { | |
222 | self.results.push(ex.span); | |
223 | return; | |
224 | } | |
225 | }, | |
226 | (StripKind::Suffix, None, Some(end)) => { | |
227 | if_chain! { | |
228 | if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, left, right) = end.kind; | |
229 | if let Some(left_arg) = len_arg(self.cx, left); | |
230 | if let ExprKind::Path(left_path) = &left_arg.kind; | |
231 | if self.cx.qpath_res(left_path, left_arg.hir_id) == self.target; | |
232 | if eq_pattern_length(self.cx, self.pattern, right); | |
233 | then { | |
234 | self.results.push(ex.span); | |
235 | return; | |
236 | } | |
237 | } | |
238 | }, | |
239 | _ => {} | |
240 | } | |
241 | } | |
242 | } | |
243 | ||
244 | walk_expr(self, ex); | |
245 | } | |
246 | } | |
247 | ||
248 | let mut finder = StrippingFinder { | |
249 | cx, | |
250 | strip_kind, | |
251 | target, | |
252 | pattern, | |
253 | results: vec![], | |
254 | }; | |
255 | walk_expr(&mut finder, expr); | |
256 | finder.results | |
257 | } |