]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/loops/manual_memcpy.rs
Rollup merge of #83092 - petrochenkov:qspan, r=estebank
[rust.git] / clippy_lints / src / loops / manual_memcpy.rs
1 use super::{get_span_of_entire_for_loop, IncrementVisitor, InitializeVisitor, MANUAL_MEMCPY};
2 use crate::utils::sugg::Sugg;
3 use crate::utils::{
4     get_enclosing_block, higher, is_type_diagnostic_item, path_to_local, snippet, span_lint_and_sugg, sugg,
5 };
6 use if_chain::if_chain;
7 use rustc_ast::ast;
8 use rustc_errors::Applicability;
9 use rustc_hir::intravisit::walk_block;
10 use rustc_hir::{BinOpKind, Block, Expr, ExprKind, HirId, Pat, PatKind, StmtKind};
11 use rustc_lint::LateContext;
12 use rustc_middle::ty::{self, Ty};
13 use rustc_span::symbol::sym;
14 use std::iter::Iterator;
15
16 /// Checks for for loops that sequentially copy items from one slice-like
17 /// object to another.
18 pub(super) fn check<'tcx>(
19     cx: &LateContext<'tcx>,
20     pat: &'tcx Pat<'_>,
21     arg: &'tcx Expr<'_>,
22     body: &'tcx Expr<'_>,
23     expr: &'tcx Expr<'_>,
24 ) -> bool {
25     if let Some(higher::Range {
26         start: Some(start),
27         end: Some(end),
28         limits,
29     }) = higher::range(arg)
30     {
31         // the var must be a single name
32         if let PatKind::Binding(_, canonical_id, _, _) = pat.kind {
33             let mut starts = vec![Start {
34                 id: canonical_id,
35                 kind: StartKind::Range,
36             }];
37
38             // This is one of few ways to return different iterators
39             // derived from: https://stackoverflow.com/questions/29760668/conditionally-iterate-over-one-of-several-possible-iterators/52064434#52064434
40             let mut iter_a = None;
41             let mut iter_b = None;
42
43             if let ExprKind::Block(block, _) = body.kind {
44                 if let Some(loop_counters) = get_loop_counters(cx, block, expr) {
45                     starts.extend(loop_counters);
46                 }
47                 iter_a = Some(get_assignments(block, &starts));
48             } else {
49                 iter_b = Some(get_assignment(body));
50             }
51
52             let assignments = iter_a.into_iter().flatten().chain(iter_b.into_iter());
53
54             let big_sugg = assignments
55                 // The only statements in the for loops can be indexed assignments from
56                 // indexed retrievals (except increments of loop counters).
57                 .map(|o| {
58                     o.and_then(|(lhs, rhs)| {
59                         let rhs = fetch_cloned_expr(rhs);
60                         if_chain! {
61                             if let ExprKind::Index(base_left, idx_left) = lhs.kind;
62                             if let ExprKind::Index(base_right, idx_right) = rhs.kind;
63                             if is_slice_like(cx, cx.typeck_results().expr_ty(base_left))
64                                 && is_slice_like(cx, cx.typeck_results().expr_ty(base_right));
65                             if let Some((start_left, offset_left)) = get_details_from_idx(cx, &idx_left, &starts);
66                             if let Some((start_right, offset_right)) = get_details_from_idx(cx, &idx_right, &starts);
67
68                             // Source and destination must be different
69                             if path_to_local(base_left) != path_to_local(base_right);
70                             then {
71                                 Some((IndexExpr { base: base_left, idx: start_left, idx_offset: offset_left },
72                                     IndexExpr { base: base_right, idx: start_right, idx_offset: offset_right }))
73                             } else {
74                                 None
75                             }
76                         }
77                     })
78                 })
79                 .map(|o| o.map(|(dst, src)| build_manual_memcpy_suggestion(cx, start, end, limits, &dst, &src)))
80                 .collect::<Option<Vec<_>>>()
81                 .filter(|v| !v.is_empty())
82                 .map(|v| v.join("\n    "));
83
84             if let Some(big_sugg) = big_sugg {
85                 span_lint_and_sugg(
86                     cx,
87                     MANUAL_MEMCPY,
88                     get_span_of_entire_for_loop(expr),
89                     "it looks like you're manually copying between slices",
90                     "try replacing the loop by",
91                     big_sugg,
92                     Applicability::Unspecified,
93                 );
94                 return true;
95             }
96         }
97     }
98     false
99 }
100
101 fn build_manual_memcpy_suggestion<'tcx>(
102     cx: &LateContext<'tcx>,
103     start: &Expr<'_>,
104     end: &Expr<'_>,
105     limits: ast::RangeLimits,
106     dst: &IndexExpr<'_>,
107     src: &IndexExpr<'_>,
108 ) -> String {
109     fn print_offset(offset: MinifyingSugg<'static>) -> MinifyingSugg<'static> {
110         if offset.as_str() == "0" {
111             sugg::EMPTY.into()
112         } else {
113             offset
114         }
115     }
116
117     let print_limit = |end: &Expr<'_>, end_str: &str, base: &Expr<'_>, sugg: MinifyingSugg<'static>| {
118         if_chain! {
119             if let ExprKind::MethodCall(method, _, len_args, _) = end.kind;
120             if method.ident.name == sym!(len);
121             if len_args.len() == 1;
122             if let Some(arg) = len_args.get(0);
123             if path_to_local(arg) == path_to_local(base);
124             then {
125                 if sugg.as_str() == end_str {
126                     sugg::EMPTY.into()
127                 } else {
128                     sugg
129                 }
130             } else {
131                 match limits {
132                     ast::RangeLimits::Closed => {
133                         sugg + &sugg::ONE.into()
134                     },
135                     ast::RangeLimits::HalfOpen => sugg,
136                 }
137             }
138         }
139     };
140
141     let start_str = Sugg::hir(cx, start, "").into();
142     let end_str: MinifyingSugg<'_> = Sugg::hir(cx, end, "").into();
143
144     let print_offset_and_limit = |idx_expr: &IndexExpr<'_>| match idx_expr.idx {
145         StartKind::Range => (
146             print_offset(apply_offset(&start_str, &idx_expr.idx_offset)).into_sugg(),
147             print_limit(
148                 end,
149                 end_str.as_str(),
150                 idx_expr.base,
151                 apply_offset(&end_str, &idx_expr.idx_offset),
152             )
153             .into_sugg(),
154         ),
155         StartKind::Counter { initializer } => {
156             let counter_start = Sugg::hir(cx, initializer, "").into();
157             (
158                 print_offset(apply_offset(&counter_start, &idx_expr.idx_offset)).into_sugg(),
159                 print_limit(
160                     end,
161                     end_str.as_str(),
162                     idx_expr.base,
163                     apply_offset(&end_str, &idx_expr.idx_offset) + &counter_start - &start_str,
164                 )
165                 .into_sugg(),
166             )
167         },
168     };
169
170     let (dst_offset, dst_limit) = print_offset_and_limit(&dst);
171     let (src_offset, src_limit) = print_offset_and_limit(&src);
172
173     let dst_base_str = snippet(cx, dst.base.span, "???");
174     let src_base_str = snippet(cx, src.base.span, "???");
175
176     let dst = if dst_offset == sugg::EMPTY && dst_limit == sugg::EMPTY {
177         dst_base_str
178     } else {
179         format!(
180             "{}[{}..{}]",
181             dst_base_str,
182             dst_offset.maybe_par(),
183             dst_limit.maybe_par()
184         )
185         .into()
186     };
187
188     format!(
189         "{}.clone_from_slice(&{}[{}..{}]);",
190         dst,
191         src_base_str,
192         src_offset.maybe_par(),
193         src_limit.maybe_par()
194     )
195 }
196
197 /// a wrapper of `Sugg`. Besides what `Sugg` do, this removes unnecessary `0`;
198 /// and also, it avoids subtracting a variable from the same one by replacing it with `0`.
199 /// it exists for the convenience of the overloaded operators while normal functions can do the
200 /// same.
201 #[derive(Clone)]
202 struct MinifyingSugg<'a>(Sugg<'a>);
203
204 impl<'a> MinifyingSugg<'a> {
205     fn as_str(&self) -> &str {
206         // HACK: Don't sync to Clippy! Required because something with the `or_patterns` feature
207         // changed and this would now require parentheses.
208         match &self.0 {
209             Sugg::NonParen(s) | Sugg::MaybeParen(s) | Sugg::BinOp(_, s) => s.as_ref(),
210         }
211     }
212
213     fn into_sugg(self) -> Sugg<'a> {
214         self.0
215     }
216 }
217
218 impl<'a> From<Sugg<'a>> for MinifyingSugg<'a> {
219     fn from(sugg: Sugg<'a>) -> Self {
220         Self(sugg)
221     }
222 }
223
224 impl std::ops::Add for &MinifyingSugg<'static> {
225     type Output = MinifyingSugg<'static>;
226     fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
227         match (self.as_str(), rhs.as_str()) {
228             ("0", _) => rhs.clone(),
229             (_, "0") => self.clone(),
230             (_, _) => (&self.0 + &rhs.0).into(),
231         }
232     }
233 }
234
235 impl std::ops::Sub for &MinifyingSugg<'static> {
236     type Output = MinifyingSugg<'static>;
237     fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
238         match (self.as_str(), rhs.as_str()) {
239             (_, "0") => self.clone(),
240             ("0", _) => (-rhs.0.clone()).into(),
241             (x, y) if x == y => sugg::ZERO.into(),
242             (_, _) => (&self.0 - &rhs.0).into(),
243         }
244     }
245 }
246
247 impl std::ops::Add<&MinifyingSugg<'static>> for MinifyingSugg<'static> {
248     type Output = MinifyingSugg<'static>;
249     fn add(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
250         match (self.as_str(), rhs.as_str()) {
251             ("0", _) => rhs.clone(),
252             (_, "0") => self,
253             (_, _) => (self.0 + &rhs.0).into(),
254         }
255     }
256 }
257
258 impl std::ops::Sub<&MinifyingSugg<'static>> for MinifyingSugg<'static> {
259     type Output = MinifyingSugg<'static>;
260     fn sub(self, rhs: &MinifyingSugg<'static>) -> MinifyingSugg<'static> {
261         match (self.as_str(), rhs.as_str()) {
262             (_, "0") => self,
263             ("0", _) => (-rhs.0.clone()).into(),
264             (x, y) if x == y => sugg::ZERO.into(),
265             (_, _) => (self.0 - &rhs.0).into(),
266         }
267     }
268 }
269
270 /// a wrapper around `MinifyingSugg`, which carries a operator like currying
271 /// so that the suggested code become more efficient (e.g. `foo + -bar` `foo - bar`).
272 struct Offset {
273     value: MinifyingSugg<'static>,
274     sign: OffsetSign,
275 }
276
277 #[derive(Clone, Copy)]
278 enum OffsetSign {
279     Positive,
280     Negative,
281 }
282
283 impl Offset {
284     fn negative(value: Sugg<'static>) -> Self {
285         Self {
286             value: value.into(),
287             sign: OffsetSign::Negative,
288         }
289     }
290
291     fn positive(value: Sugg<'static>) -> Self {
292         Self {
293             value: value.into(),
294             sign: OffsetSign::Positive,
295         }
296     }
297
298     fn empty() -> Self {
299         Self::positive(sugg::ZERO)
300     }
301 }
302
303 fn apply_offset(lhs: &MinifyingSugg<'static>, rhs: &Offset) -> MinifyingSugg<'static> {
304     match rhs.sign {
305         OffsetSign::Positive => lhs + &rhs.value,
306         OffsetSign::Negative => lhs - &rhs.value,
307     }
308 }
309
310 #[derive(Debug, Clone, Copy)]
311 enum StartKind<'hir> {
312     Range,
313     Counter { initializer: &'hir Expr<'hir> },
314 }
315
316 struct IndexExpr<'hir> {
317     base: &'hir Expr<'hir>,
318     idx: StartKind<'hir>,
319     idx_offset: Offset,
320 }
321
322 struct Start<'hir> {
323     id: HirId,
324     kind: StartKind<'hir>,
325 }
326
327 fn is_slice_like<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'_>) -> bool {
328     let is_slice = match ty.kind() {
329         ty::Ref(_, subty, _) => is_slice_like(cx, subty),
330         ty::Slice(..) | ty::Array(..) => true,
331         _ => false,
332     };
333
334     is_slice || is_type_diagnostic_item(cx, ty, sym::vec_type) || is_type_diagnostic_item(cx, ty, sym::vecdeque_type)
335 }
336
337 fn fetch_cloned_expr<'tcx>(expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
338     if_chain! {
339         if let ExprKind::MethodCall(method, _, args, _) = expr.kind;
340         if method.ident.name == sym::clone;
341         if args.len() == 1;
342         if let Some(arg) = args.get(0);
343         then { arg } else { expr }
344     }
345 }
346
347 fn get_details_from_idx<'tcx>(
348     cx: &LateContext<'tcx>,
349     idx: &Expr<'_>,
350     starts: &[Start<'tcx>],
351 ) -> Option<(StartKind<'tcx>, Offset)> {
352     fn get_start<'tcx>(e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<StartKind<'tcx>> {
353         let id = path_to_local(e)?;
354         starts.iter().find(|start| start.id == id).map(|start| start.kind)
355     }
356
357     fn get_offset<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<Sugg<'static>> {
358         match &e.kind {
359             ExprKind::Lit(l) => match l.node {
360                 ast::LitKind::Int(x, _ty) => Some(Sugg::NonParen(x.to_string().into())),
361                 _ => None,
362             },
363             ExprKind::Path(..) if get_start(e, starts).is_none() => Some(Sugg::hir(cx, e, "???")),
364             _ => None,
365         }
366     }
367
368     match idx.kind {
369         ExprKind::Binary(op, lhs, rhs) => match op.node {
370             BinOpKind::Add => {
371                 let offset_opt = get_start(lhs, starts)
372                     .and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, o)))
373                     .or_else(|| get_start(rhs, starts).and_then(|s| get_offset(cx, lhs, starts).map(|o| (s, o))));
374
375                 offset_opt.map(|(s, o)| (s, Offset::positive(o)))
376             },
377             BinOpKind::Sub => {
378                 get_start(lhs, starts).and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, Offset::negative(o))))
379             },
380             _ => None,
381         },
382         ExprKind::Path(..) => get_start(idx, starts).map(|s| (s, Offset::empty())),
383         _ => None,
384     }
385 }
386
387 fn get_assignment<'tcx>(e: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> {
388     if let ExprKind::Assign(lhs, rhs, _) = e.kind {
389         Some((lhs, rhs))
390     } else {
391         None
392     }
393 }
394
395 /// Get assignments from the given block.
396 /// The returned iterator yields `None` if no assignment expressions are there,
397 /// filtering out the increments of the given whitelisted loop counters;
398 /// because its job is to make sure there's nothing other than assignments and the increments.
399 fn get_assignments<'a, 'tcx>(
400     Block { stmts, expr, .. }: &'tcx Block<'tcx>,
401     loop_counters: &'a [Start<'tcx>],
402 ) -> impl Iterator<Item = Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)>> + 'a {
403     // As the `filter` and `map` below do different things, I think putting together
404     // just increases complexity. (cc #3188 and #4193)
405     stmts
406         .iter()
407         .filter_map(move |stmt| match stmt.kind {
408             StmtKind::Local(..) | StmtKind::Item(..) => None,
409             StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e),
410         })
411         .chain((*expr).into_iter())
412         .filter(move |e| {
413             if let ExprKind::AssignOp(_, place, _) = e.kind {
414                 path_to_local(place).map_or(false, |id| {
415                     !loop_counters
416                         .iter()
417                         // skip the first item which should be `StartKind::Range`
418                         // this makes it possible to use the slice with `StartKind::Range` in the same iterator loop.
419                         .skip(1)
420                         .any(|counter| counter.id == id)
421                 })
422             } else {
423                 true
424             }
425         })
426         .map(get_assignment)
427 }
428
429 fn get_loop_counters<'a, 'tcx>(
430     cx: &'a LateContext<'tcx>,
431     body: &'tcx Block<'tcx>,
432     expr: &'tcx Expr<'_>,
433 ) -> Option<impl Iterator<Item = Start<'tcx>> + 'a> {
434     // Look for variables that are incremented once per loop iteration.
435     let mut increment_visitor = IncrementVisitor::new(cx);
436     walk_block(&mut increment_visitor, body);
437
438     // For each candidate, check the parent block to see if
439     // it's initialized to zero at the start of the loop.
440     get_enclosing_block(&cx, expr.hir_id).and_then(|block| {
441         increment_visitor
442             .into_results()
443             .filter_map(move |var_id| {
444                 let mut initialize_visitor = InitializeVisitor::new(cx, expr, var_id);
445                 walk_block(&mut initialize_visitor, block);
446
447                 initialize_visitor.get_result().map(|(_, initializer)| Start {
448                     id: var_id,
449                     kind: StartKind::Counter { initializer },
450                 })
451             })
452             .into()
453     })
454 }