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