Skip to content

Commit 5b870e1

Browse files
nahuakangY-Nak
authored andcommitted
Move detect_manual_memcpy to its module; split up utils structs
1 parent 2c1f676 commit 5b870e1

File tree

3 files changed

+694
-673
lines changed

3 files changed

+694
-673
lines changed
Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
use super::{get_span_of_entire_for_loop, IncrementVisitor, InitializeVisitor, MinifyingSugg};
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 detect_manual_memcpy<'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+
super::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 around `MinifyingSugg`, which carries a operator like currying
198+
/// so that the suggested code become more efficient (e.g. `foo + -bar` `foo - bar`).
199+
struct Offset {
200+
value: MinifyingSugg<'static>,
201+
sign: OffsetSign,
202+
}
203+
204+
#[derive(Clone, Copy)]
205+
enum OffsetSign {
206+
Positive,
207+
Negative,
208+
}
209+
210+
impl Offset {
211+
fn negative(value: Sugg<'static>) -> Self {
212+
Self {
213+
value: value.into(),
214+
sign: OffsetSign::Negative,
215+
}
216+
}
217+
218+
fn positive(value: Sugg<'static>) -> Self {
219+
Self {
220+
value: value.into(),
221+
sign: OffsetSign::Positive,
222+
}
223+
}
224+
225+
fn empty() -> Self {
226+
Self::positive(sugg::ZERO)
227+
}
228+
}
229+
230+
fn apply_offset(lhs: &MinifyingSugg<'static>, rhs: &Offset) -> MinifyingSugg<'static> {
231+
match rhs.sign {
232+
OffsetSign::Positive => lhs + &rhs.value,
233+
OffsetSign::Negative => lhs - &rhs.value,
234+
}
235+
}
236+
237+
#[derive(Debug, Clone, Copy)]
238+
enum StartKind<'hir> {
239+
Range,
240+
Counter { initializer: &'hir Expr<'hir> },
241+
}
242+
243+
struct IndexExpr<'hir> {
244+
base: &'hir Expr<'hir>,
245+
idx: StartKind<'hir>,
246+
idx_offset: Offset,
247+
}
248+
249+
struct Start<'hir> {
250+
id: HirId,
251+
kind: StartKind<'hir>,
252+
}
253+
254+
fn is_slice_like<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'_>) -> bool {
255+
let is_slice = match ty.kind() {
256+
ty::Ref(_, subty, _) => is_slice_like(cx, subty),
257+
ty::Slice(..) | ty::Array(..) => true,
258+
_ => false,
259+
};
260+
261+
is_slice || is_type_diagnostic_item(cx, ty, sym::vec_type) || is_type_diagnostic_item(cx, ty, sym!(vecdeque_type))
262+
}
263+
264+
fn fetch_cloned_expr<'tcx>(expr: &'tcx Expr<'tcx>) -> &'tcx Expr<'tcx> {
265+
if_chain! {
266+
if let ExprKind::MethodCall(method, _, args, _) = expr.kind;
267+
if method.ident.name == sym::clone;
268+
if args.len() == 1;
269+
if let Some(arg) = args.get(0);
270+
then { arg } else { expr }
271+
}
272+
}
273+
274+
fn get_details_from_idx<'tcx>(
275+
cx: &LateContext<'tcx>,
276+
idx: &Expr<'_>,
277+
starts: &[Start<'tcx>],
278+
) -> Option<(StartKind<'tcx>, Offset)> {
279+
fn get_start<'tcx>(e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<StartKind<'tcx>> {
280+
let id = path_to_local(e)?;
281+
starts.iter().find(|start| start.id == id).map(|start| start.kind)
282+
}
283+
284+
fn get_offset<'tcx>(cx: &LateContext<'tcx>, e: &Expr<'_>, starts: &[Start<'tcx>]) -> Option<Sugg<'static>> {
285+
match &e.kind {
286+
ExprKind::Lit(l) => match l.node {
287+
ast::LitKind::Int(x, _ty) => Some(Sugg::NonParen(x.to_string().into())),
288+
_ => None,
289+
},
290+
ExprKind::Path(..) if get_start(e, starts).is_none() => Some(Sugg::hir(cx, e, "???")),
291+
_ => None,
292+
}
293+
}
294+
295+
match idx.kind {
296+
ExprKind::Binary(op, lhs, rhs) => match op.node {
297+
BinOpKind::Add => {
298+
let offset_opt = get_start(lhs, starts)
299+
.and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, o)))
300+
.or_else(|| get_start(rhs, starts).and_then(|s| get_offset(cx, lhs, starts).map(|o| (s, o))));
301+
302+
offset_opt.map(|(s, o)| (s, Offset::positive(o)))
303+
},
304+
BinOpKind::Sub => {
305+
get_start(lhs, starts).and_then(|s| get_offset(cx, rhs, starts).map(|o| (s, Offset::negative(o))))
306+
},
307+
_ => None,
308+
},
309+
ExprKind::Path(..) => get_start(idx, starts).map(|s| (s, Offset::empty())),
310+
_ => None,
311+
}
312+
}
313+
314+
fn get_assignment<'tcx>(e: &'tcx Expr<'tcx>) -> Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)> {
315+
if let ExprKind::Assign(lhs, rhs, _) = e.kind {
316+
Some((lhs, rhs))
317+
} else {
318+
None
319+
}
320+
}
321+
322+
/// Get assignments from the given block.
323+
/// The returned iterator yields `None` if no assignment expressions are there,
324+
/// filtering out the increments of the given whitelisted loop counters;
325+
/// because its job is to make sure there's nothing other than assignments and the increments.
326+
fn get_assignments<'a, 'tcx>(
327+
Block { stmts, expr, .. }: &'tcx Block<'tcx>,
328+
loop_counters: &'a [Start<'tcx>],
329+
) -> impl Iterator<Item = Option<(&'tcx Expr<'tcx>, &'tcx Expr<'tcx>)>> + 'a {
330+
// As the `filter` and `map` below do different things, I think putting together
331+
// just increases complexity. (cc #3188 and #4193)
332+
stmts
333+
.iter()
334+
.filter_map(move |stmt| match stmt.kind {
335+
StmtKind::Local(..) | StmtKind::Item(..) => None,
336+
StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e),
337+
})
338+
.chain((*expr).into_iter())
339+
.filter(move |e| {
340+
if let ExprKind::AssignOp(_, place, _) = e.kind {
341+
path_to_local(place).map_or(false, |id| {
342+
!loop_counters
343+
.iter()
344+
// skip the first item which should be `StartKind::Range`
345+
// this makes it possible to use the slice with `StartKind::Range` in the same iterator loop.
346+
.skip(1)
347+
.any(|counter| counter.id == id)
348+
})
349+
} else {
350+
true
351+
}
352+
})
353+
.map(get_assignment)
354+
}
355+
356+
fn get_loop_counters<'a, 'tcx>(
357+
cx: &'a LateContext<'tcx>,
358+
body: &'tcx Block<'tcx>,
359+
expr: &'tcx Expr<'_>,
360+
) -> Option<impl Iterator<Item = Start<'tcx>> + 'a> {
361+
// Look for variables that are incremented once per loop iteration.
362+
let mut increment_visitor = IncrementVisitor::new(cx);
363+
walk_block(&mut increment_visitor, body);
364+
365+
// For each candidate, check the parent block to see if
366+
// it's initialized to zero at the start of the loop.
367+
get_enclosing_block(&cx, expr.hir_id).and_then(|block| {
368+
increment_visitor
369+
.into_results()
370+
.filter_map(move |var_id| {
371+
let mut initialize_visitor = InitializeVisitor::new(cx, expr, var_id);
372+
walk_block(&mut initialize_visitor, block);
373+
374+
initialize_visitor.get_result().map(|(_, initializer)| Start {
375+
id: var_id,
376+
kind: StartKind::Counter { initializer },
377+
})
378+
})
379+
.into()
380+
})
381+
}

0 commit comments

Comments
 (0)