Skip to content

Commit 90f345d

Browse files
marcusklaasoli-obk
authored andcommitted
Add lint to detect manual slice copies
1 parent b326317 commit 90f345d

File tree

2 files changed

+368
-57
lines changed

2 files changed

+368
-57
lines changed

clippy_lints/src/loops.rs

Lines changed: 318 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use itertools::Itertools;
12
use reexport::*;
23
use rustc::hir::*;
34
use rustc::hir::def::Def;
@@ -15,10 +16,29 @@ use syntax::ast;
1516
use utils::sugg;
1617

1718
use utils::{get_enclosing_block, get_parent_expr, higher, in_external_macro, is_integer_literal, is_refutable,
18-
last_path_segment, match_trait_method, match_type, multispan_sugg, snippet, span_help_and_lint, span_lint,
19-
span_lint_and_sugg, span_lint_and_then};
19+
last_path_segment, match_trait_method, match_type, multispan_sugg, snippet, snippet_opt,
20+
span_help_and_lint, span_lint, span_lint_and_sugg, span_lint_and_then};
2021
use utils::paths;
2122

23+
/// **What it does:** Checks for for loops that manually copy items between
24+
/// slices that could be optimized by having a memcpy.
25+
///
26+
/// **Why is this bad?** It is not as fast as a memcpy.
27+
///
28+
/// **Known problems:** None.
29+
///
30+
/// **Example:**
31+
/// ```rust
32+
/// for i in 0..src.len() {
33+
/// dst[i + 64] = src[i];
34+
/// }
35+
/// ```
36+
declare_lint! {
37+
pub MANUAL_MEMCPY,
38+
Warn,
39+
"manually copying items between slices"
40+
}
41+
2242
/// **What it does:** Checks for looping over the range of `0..len` of some
2343
/// collection just to get the values by index.
2444
///
@@ -314,6 +334,7 @@ pub struct Pass;
314334
impl LintPass for Pass {
315335
fn get_lints(&self) -> LintArray {
316336
lint_array!(
337+
MANUAL_MEMCPY,
317338
NEEDLESS_RANGE_LOOP,
318339
EXPLICIT_ITER_LOOP,
319340
EXPLICIT_INTO_ITER_LOOP,
@@ -570,6 +591,249 @@ fn check_for_loop<'a, 'tcx>(
570591
check_for_loop_arg(cx, pat, arg, expr);
571592
check_for_loop_explicit_counter(cx, arg, body, expr);
572593
check_for_loop_over_map_kv(cx, pat, arg, body, expr);
594+
detect_manual_memcpy(cx, pat, arg, body, expr);
595+
}
596+
597+
fn same_var<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &Expr, var: DefId) -> bool {
598+
if_let_chain! {[
599+
let ExprPath(ref qpath) = expr.node,
600+
let QPath::Resolved(None, ref path) = *qpath,
601+
path.segments.len() == 1,
602+
// our variable!
603+
cx.tables.qpath_def(qpath, expr.hir_id).def_id() == var
604+
], {
605+
return true;
606+
}}
607+
608+
false
609+
}
610+
611+
struct Offset {
612+
value: String,
613+
negate: bool,
614+
}
615+
616+
impl Offset {
617+
fn negative(s: String) -> Self {
618+
Self {
619+
value: s,
620+
negate: true,
621+
}
622+
}
623+
624+
fn positive(s: String) -> Self {
625+
Self {
626+
value: s,
627+
negate: false,
628+
}
629+
}
630+
}
631+
632+
struct FixedOffsetVar {
633+
var_name: String,
634+
offset: Offset,
635+
}
636+
637+
fn is_slice_like<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, ty: Ty) -> bool {
638+
let is_slice = match ty.sty {
639+
ty::TyRef(_, ref subty) => is_slice_like(cx, subty.ty),
640+
ty::TySlice(..) | ty::TyArray(..) => true,
641+
_ => false,
642+
};
643+
644+
is_slice || match_type(cx, ty, &paths::VEC) || match_type(cx, ty, &paths::VEC_DEQUE)
645+
}
646+
647+
fn get_fixed_offset_var<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, expr: &Expr, var: DefId) -> Option<FixedOffsetVar> {
648+
fn extract_offset<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, e: &Expr, var: DefId) -> Option<String> {
649+
match e.node {
650+
ExprLit(ref l) => match l.node {
651+
ast::LitKind::Int(x, _ty) => Some(x.to_string()),
652+
_ => None,
653+
},
654+
ExprPath(..) if !same_var(cx, e, var) => Some(snippet_opt(cx, e.span).unwrap_or_else(|| "??".into())),
655+
_ => None,
656+
}
657+
}
658+
659+
if let ExprIndex(ref seqexpr, ref idx) = expr.node {
660+
let ty = cx.tables.expr_ty(seqexpr);
661+
if !is_slice_like(cx, ty) {
662+
return None;
663+
}
664+
665+
let offset = match idx.node {
666+
ExprBinary(op, ref lhs, ref rhs) => match op.node {
667+
BinOp_::BiAdd => {
668+
let offset_opt = if same_var(cx, lhs, var) {
669+
extract_offset(cx, rhs, var)
670+
} else if same_var(cx, rhs, var) {
671+
extract_offset(cx, lhs, var)
672+
} else {
673+
None
674+
};
675+
676+
offset_opt.map(Offset::positive)
677+
},
678+
BinOp_::BiSub if same_var(cx, lhs, var) => extract_offset(cx, rhs, var).map(Offset::negative),
679+
_ => None,
680+
},
681+
ExprPath(..) => if same_var(cx, idx, var) {
682+
Some(Offset::positive("0".into()))
683+
} else {
684+
None
685+
},
686+
_ => None,
687+
};
688+
689+
offset.map(|o| {
690+
FixedOffsetVar {
691+
var_name: snippet_opt(cx, seqexpr.span).unwrap_or_else(|| "???".into()),
692+
offset: o,
693+
}
694+
})
695+
} else {
696+
None
697+
}
698+
}
699+
700+
fn get_indexed_assignments<'a, 'tcx>(
701+
cx: &LateContext<'a, 'tcx>,
702+
body: &Expr,
703+
var: DefId,
704+
) -> Vec<(FixedOffsetVar, FixedOffsetVar)> {
705+
fn get_assignment<'a, 'tcx>(
706+
cx: &LateContext<'a, 'tcx>,
707+
e: &Expr,
708+
var: DefId,
709+
) -> Option<(FixedOffsetVar, FixedOffsetVar)> {
710+
if let Expr_::ExprAssign(ref lhs, ref rhs) = e.node {
711+
match (get_fixed_offset_var(cx, lhs, var), get_fixed_offset_var(cx, rhs, var)) {
712+
(Some(offset_left), Some(offset_right)) => Some((offset_left, offset_right)),
713+
_ => None,
714+
}
715+
} else {
716+
None
717+
}
718+
}
719+
720+
if let Expr_::ExprBlock(ref b) = body.node {
721+
let Block {
722+
ref stmts,
723+
ref expr,
724+
..
725+
} = **b;
726+
727+
stmts
728+
.iter()
729+
.map(|stmt| match stmt.node {
730+
Stmt_::StmtDecl(..) => None,
731+
Stmt_::StmtExpr(ref e, _node_id) | Stmt_::StmtSemi(ref e, _node_id) => Some(get_assignment(cx, e, var)),
732+
})
733+
.chain(
734+
expr.as_ref()
735+
.into_iter()
736+
.map(|e| Some(get_assignment(cx, &*e, var))),
737+
)
738+
.filter_map(|op| op)
739+
.collect::<Option<Vec<_>>>()
740+
.unwrap_or_else(|| vec![])
741+
} else {
742+
get_assignment(cx, body, var).into_iter().collect()
743+
}
744+
}
745+
746+
/// Check for for loops that sequentially copy items from one slice-like
747+
/// object to another.
748+
fn detect_manual_memcpy<'a, 'tcx>(
749+
cx: &LateContext<'a, 'tcx>,
750+
pat: &'tcx Pat,
751+
arg: &'tcx Expr,
752+
body: &'tcx Expr,
753+
expr: &'tcx Expr,
754+
) {
755+
if let Some(higher::Range {
756+
start: Some(start),
757+
ref end,
758+
limits,
759+
}) = higher::range(arg)
760+
{
761+
// the var must be a single name
762+
if let PatKind::Binding(_, def_id, _, _) = pat.node {
763+
let print_sum = |arg1: &Offset, arg2: &Offset| -> String {
764+
match (&arg1.value[..], arg1.negate, &arg2.value[..], arg2.negate) {
765+
("0", _, "0", _) => "".into(),
766+
("0", _, x, false) | (x, false, "0", false) => x.into(),
767+
("0", _, x, true) | (x, false, "0", true) => format!("-{}", x),
768+
(x, false, y, false) => format!("({} + {})", x, y),
769+
(x, false, y, true) => format!("({} - {})", x, y),
770+
(x, true, y, false) => format!("({} - {})", y, x),
771+
(x, true, y, true) => format!("-({} + {})", x, y),
772+
}
773+
};
774+
775+
let print_limit = |end: &Option<&Expr>, offset: Offset, var_name: &str| if let Some(end) = *end {
776+
if_let_chain! {[
777+
let ExprMethodCall(ref method, _, ref len_args) = end.node,
778+
method.name == "len",
779+
len_args.len() == 1,
780+
let Some(arg) = len_args.get(0),
781+
snippet(cx, arg.span, "??") == var_name,
782+
], {
783+
return if offset.negate {
784+
format!("({} - {})", snippet(cx, end.span, "<src>.len()"), offset.value)
785+
} else {
786+
"".to_owned()
787+
};
788+
}}
789+
790+
let end_str = match limits {
791+
ast::RangeLimits::Closed => {
792+
let end = sugg::Sugg::hir(cx, end, "<count>");
793+
format!("{}", end + sugg::ONE)
794+
},
795+
ast::RangeLimits::HalfOpen => format!("{}", snippet(cx, end.span, "..")),
796+
};
797+
798+
print_sum(&Offset::positive(end_str), &offset)
799+
} else {
800+
"..".into()
801+
};
802+
803+
// The only statements in the for loops can be indexed assignments from
804+
// indexed retrievals.
805+
let manual_copies = get_indexed_assignments(cx, body, def_id);
806+
807+
let big_sugg = manual_copies
808+
.into_iter()
809+
.map(|(dst_var, src_var)| {
810+
let start_str = Offset::positive(snippet_opt(cx, start.span).unwrap_or_else(|| "".into()));
811+
let dst_offset = print_sum(&start_str, &dst_var.offset);
812+
let dst_limit = print_limit(end, dst_var.offset, &dst_var.var_name);
813+
let src_offset = print_sum(&start_str, &src_var.offset);
814+
let src_limit = print_limit(end, src_var.offset, &src_var.var_name);
815+
let dst = if dst_offset == "" && dst_limit == "" {
816+
dst_var.var_name
817+
} else {
818+
format!("{}[{}..{}]", dst_var.var_name, dst_offset, dst_limit)
819+
};
820+
821+
format!("{}.clone_from_slice(&{}[{}..{}])", dst, src_var.var_name, src_offset, src_limit)
822+
})
823+
.join("\n ");
824+
825+
if !big_sugg.is_empty() {
826+
span_lint_and_sugg(
827+
cx,
828+
MANUAL_MEMCPY,
829+
expr.span,
830+
"it looks like you're manually copying between slices",
831+
"try replacing the loop by",
832+
big_sugg,
833+
);
834+
}
835+
}
836+
}
573837
}
574838

575839
/// Check for looping over a range and then indexing a sequence with it.
@@ -1024,9 +1288,29 @@ impl<'tcx> Visitor<'tcx> for UsedVisitor {
10241288
fn visit_expr(&mut self, expr: &'tcx Expr) {
10251289
if match_var(expr, self.var) {
10261290
self.used = true;
1027-
return;
1291+
} else {
1292+
walk_expr(self, expr);
1293+
}
1294+
}
1295+
1296+
fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'tcx> {
1297+
NestedVisitorMap::None
1298+
}
1299+
}
1300+
1301+
struct DefIdUsedVisitor<'a, 'tcx: 'a> {
1302+
cx: &'a LateContext<'a, 'tcx>,
1303+
def_id: DefId,
1304+
used: bool,
1305+
}
1306+
1307+
impl<'a, 'tcx: 'a> Visitor<'tcx> for DefIdUsedVisitor<'a, 'tcx> {
1308+
fn visit_expr(&mut self, expr: &'tcx Expr) {
1309+
if same_var(self.cx, expr, self.def_id) {
1310+
self.used = true;
1311+
} else {
1312+
walk_expr(self, expr);
10281313
}
1029-
walk_expr(self, expr);
10301314
}
10311315

10321316
fn nested_visit_map<'this>(&'this mut self) -> NestedVisitorMap<'this, 'tcx> {
@@ -1054,40 +1338,46 @@ impl<'a, 'tcx> Visitor<'tcx> for VarVisitor<'a, 'tcx> {
10541338
if_let_chain! {[
10551339
// an index op
10561340
let ExprIndex(ref seqexpr, ref idx) = expr.node,
1057-
// directly indexing a variable
1058-
let ExprPath(ref qpath) = idx.node,
1059-
let QPath::Resolved(None, ref path) = *qpath,
1060-
path.segments.len() == 1,
1061-
// our variable!
1062-
self.cx.tables.qpath_def(qpath, expr.hir_id).def_id() == self.var,
10631341
// the indexed container is referenced by a name
10641342
let ExprPath(ref seqpath) = seqexpr.node,
10651343
let QPath::Resolved(None, ref seqvar) = *seqpath,
10661344
seqvar.segments.len() == 1,
10671345
], {
1068-
let def = self.cx.tables.qpath_def(seqpath, seqexpr.hir_id);
1069-
match def {
1070-
Def::Local(..) | Def::Upvar(..) => {
1071-
let def_id = def.def_id();
1072-
let node_id = self.cx.tcx.hir.as_local_node_id(def_id).expect("local/upvar are local nodes");
1073-
let hir_id = self.cx.tcx.hir.node_to_hir_id(node_id);
1074-
1075-
let parent_id = self.cx.tcx.hir.get_parent(expr.id);
1076-
let parent_def_id = self.cx.tcx.hir.local_def_id(parent_id);
1077-
let extent = self.cx.tcx.region_scope_tree(parent_def_id).var_scope(hir_id.local_id);
1078-
self.indexed.insert(seqvar.segments[0].name, Some(extent));
1079-
return; // no need to walk further
1080-
}
1081-
Def::Static(..) | Def::Const(..) => {
1082-
self.indexed.insert(seqvar.segments[0].name, None);
1083-
return; // no need to walk further
1346+
let index_used = same_var(self.cx, idx, self.var) || {
1347+
let mut used_visitor = DefIdUsedVisitor {
1348+
cx: self.cx,
1349+
def_id: self.var,
1350+
used: false,
1351+
};
1352+
walk_expr(&mut used_visitor, idx);
1353+
used_visitor.used
1354+
};
1355+
1356+
if index_used {
1357+
let def = self.cx.tables.qpath_def(seqpath, seqexpr.hir_id);
1358+
match def {
1359+
Def::Local(..) | Def::Upvar(..) => {
1360+
let def_id = def.def_id();
1361+
let node_id = self.cx.tcx.hir.as_local_node_id(def_id).expect("local/upvar are local nodes");
1362+
let hir_id = self.cx.tcx.hir.node_to_hir_id(node_id);
1363+
1364+
let parent_id = self.cx.tcx.hir.get_parent(expr.id);
1365+
let parent_def_id = self.cx.tcx.hir.local_def_id(parent_id);
1366+
let extent = self.cx.tcx.region_scope_tree(parent_def_id).var_scope(hir_id.local_id);
1367+
self.indexed.insert(seqvar.segments[0].name, Some(extent));
1368+
return; // no need to walk further *on the variable*
1369+
}
1370+
Def::Static(..) | Def::Const(..) => {
1371+
self.indexed.insert(seqvar.segments[0].name, None);
1372+
return; // no need to walk further *on the variable*
1373+
}
1374+
_ => (),
10841375
}
1085-
_ => (),
10861376
}
10871377
}}
10881378

10891379
if_let_chain! {[
1090-
// directly indexing a variable
1380+
// directly using a variable
10911381
let ExprPath(ref qpath) = expr.node,
10921382
let QPath::Resolved(None, ref path) = *qpath,
10931383
path.segments.len() == 1,

0 commit comments

Comments
 (0)