1 use std::collections::VecDeque;
3 use clippy_utils::diagnostics::span_lint_and_sugg;
4 use itertools::{izip, Itertools};
5 use rustc_ast::{walk_list, Label, Mutability};
6 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
7 use rustc_errors::Applicability;
8 use rustc_hir::def::Res;
9 use rustc_hir::definitions::{DefPathData, DisambiguatedDefPathData};
10 use rustc_hir::intravisit::{walk_expr, FnKind, Visitor};
12 Arm, Block, Body, Expr, ExprKind, Guard, HirId, Let, Local, Pat, PatKind, Path, PathSegment, QPath, Stmt, StmtKind,
15 use rustc_lint::{LateContext, LateLintPass};
17 use rustc_middle::ty::{Ty, TyCtxt, TypeckResults};
18 use rustc_session::{declare_lint_pass, declare_tool_lint};
19 use rustc_span::symbol::kw;
20 use rustc_span::symbol::Ident;
23 declare_clippy_lint! {
25 /// Checks for arguments that are only used in recursion with no side-effects.
27 /// ### Why is this bad?
28 /// It could contain a useless calculation and can make function simpler.
30 /// The arguments can be involved in calculations and assignments but as long as
31 /// the calculations have no side-effects (function calls or mutating dereference)
32 /// and the assigned variables are also only in recursion, it is useless.
34 /// ### Known problems
35 /// In some cases, this would not catch all useless arguments.
38 /// fn foo(a: usize, b: usize) -> usize {
39 /// let f = |x| x + 1;
49 /// For example, the argument `b` is only used in recursion, but the lint would not catch it.
51 /// List of some examples that can not be caught:
52 /// - binary operation of non-primitive types
54 /// - some `break` relative operations
55 /// - struct pattern binding
57 /// Also, when you recurse the function name with path segments, it is not possible to detect.
61 /// fn f(a: usize, b: usize) -> usize {
69 /// # print!("{}", f(1, 1));
74 /// fn f(a: usize) -> usize {
82 /// # print!("{}", f(1));
85 #[clippy::version = "1.60.0"]
86 pub ONLY_USED_IN_RECURSION,
88 "arguments that is only used in recursion can be removed"
90 declare_lint_pass!(OnlyUsedInRecursion => [ONLY_USED_IN_RECURSION]);
92 impl<'tcx> LateLintPass<'tcx> for OnlyUsedInRecursion {
95 cx: &LateContext<'tcx>,
97 _: &'tcx rustc_hir::FnDecl<'tcx>,
98 body: &'tcx Body<'tcx>,
102 if let FnKind::ItemFn(ident, ..) | FnKind::Method(ident, ..) = kind {
103 let data = cx.tcx.def_path(cx.tcx.hir().local_def_id(id).to_def_id()).data;
105 match data.get(data.len() - 2) {
106 Some(DisambiguatedDefPathData {
107 data: DefPathData::Impl,
109 }) if *disambiguator != 0 => return,
114 let ty_res = cx.typeck_results();
115 let param_span = body
119 let mut v = Vec::new();
120 param.pat.each_binding(|_, hir_id, span, ident| {
121 v.push((hir_id, span, ident));
126 FnKind::Method(..) => 1,
129 .filter(|(_, _, ident)| !ident.name.as_str().starts_with('_'))
132 let params = body.params.iter().map(|param| param.pat).collect();
134 let mut visitor = SideEffectVisit {
135 graph: FxHashMap::default(),
136 has_side_effect: FxHashSet::default(),
137 ret_vars: Vec::new(),
138 contains_side_effect: false,
139 break_vars: FxHashMap::default(),
142 is_method: matches!(kind, FnKind::Method(..)),
147 visitor.visit_expr(&body.value);
148 let vars = std::mem::take(&mut visitor.ret_vars);
149 // this would set the return variables to side effect
150 visitor.add_side_effect(vars);
152 let mut queue = visitor.has_side_effect.iter().copied().collect::<VecDeque<_>>();
154 // a simple BFS to check all the variables that have side effect
155 while let Some(id) = queue.pop_front() {
156 if let Some(next) = visitor.graph.get(&id) {
158 if !visitor.has_side_effect.contains(i) {
159 visitor.has_side_effect.insert(*i);
166 for (id, span, ident) in param_span {
167 // if the variable is not used in recursion, it would be marked as unused
168 if !visitor.has_side_effect.contains(&id) {
169 let mut queue = VecDeque::new();
170 let mut visited = FxHashSet::default();
174 // a simple BFS to check the graph can reach to itself
175 // if it can't, it means the variable is never used in recursion
176 while let Some(id) = queue.pop_front() {
177 if let Some(next) = visitor.graph.get(&id) {
179 if !visited.contains(i) {
187 if visited.contains(&id) {
190 ONLY_USED_IN_RECURSION,
192 "parameter is only used in recursion",
193 "if this is intentional, prefix with an underscore",
194 format!("_{}", ident.name.as_str()),
195 Applicability::MaybeIncorrect,
204 pub fn is_primitive(ty: Ty<'_>) -> bool {
206 ty::Bool | ty::Char | ty::Int(_) | ty::Uint(_) | ty::Float(_) | ty::Str => true,
207 ty::Ref(_, t, _) => is_primitive(*t),
212 pub fn is_array(ty: Ty<'_>) -> bool {
214 ty::Array(..) | ty::Slice(..) => true,
215 ty::Ref(_, t, _) => is_array(*t),
220 /// This builds the graph of side effect.
221 /// The edge `a -> b` means if `a` has side effect, `b` will have side effect.
223 /// There are some exmaple in following code:
226 /// let a = b; // a -> b
227 /// let (c, d) = (a, b); // c -> b, d -> b
229 /// let e = if a == 0 { // e -> a
235 pub struct SideEffectVisit<'tcx> {
236 graph: FxHashMap<HirId, FxHashSet<HirId>>,
237 has_side_effect: FxHashSet<HirId>,
238 // bool for if the variable was dereferenced from mutable reference
239 ret_vars: Vec<(HirId, bool)>,
240 contains_side_effect: bool,
242 break_vars: FxHashMap<Ident, Vec<(HirId, bool)>>,
243 params: Vec<&'tcx Pat<'tcx>>,
246 ty_res: &'tcx TypeckResults<'tcx>,
247 ty_ctx: TyCtxt<'tcx>,
250 impl<'tcx> Visitor<'tcx> for SideEffectVisit<'tcx> {
251 fn visit_block(&mut self, b: &'tcx Block<'tcx>) {
252 b.stmts.iter().for_each(|stmt| {
253 self.visit_stmt(stmt);
254 self.ret_vars.clear();
256 walk_list!(self, visit_expr, b.expr);
259 fn visit_stmt(&mut self, s: &'tcx Stmt<'tcx>) {
261 StmtKind::Local(Local {
262 pat, init: Some(init), ..
264 self.visit_pat_expr(pat, init, false);
266 StmtKind::Item(i) => {
267 let item = self.ty_ctx.hir().item(i);
268 self.visit_item(item);
270 StmtKind::Expr(e) | StmtKind::Semi(e) => self.visit_expr(e),
271 StmtKind::Local(_) => {},
275 fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) {
276 debug_assert!(self.ret_vars.is_empty());
278 ExprKind::Array(exprs) | ExprKind::Tup(exprs) => {
279 self.ret_vars = exprs
282 self.visit_expr(expr);
283 std::mem::take(&mut self.ret_vars)
287 ExprKind::Call(callee, args) => self.visit_fn(callee, args),
288 ExprKind::MethodCall(path, args, _) => self.visit_method_call(path, args),
289 ExprKind::Binary(_, lhs, rhs) => {
290 self.visit_bin_op(lhs, rhs);
292 ExprKind::Unary(op, expr) => self.visit_un_op(op, expr),
293 ExprKind::Let(Let { pat, init, .. }) => self.visit_pat_expr(pat, init, false),
294 ExprKind::If(bind, then_expr, else_expr) => {
295 self.visit_if(bind, then_expr, else_expr);
297 ExprKind::Match(expr, arms, _) => self.visit_match(expr, arms),
298 // since analysing the closure is not easy, just set all variables in it to side-effect
299 ExprKind::Closure(_, _, body_id, _, _) => {
300 let body = self.ty_ctx.hir().body(body_id);
301 self.visit_body(body);
302 let vars = std::mem::take(&mut self.ret_vars);
303 self.add_side_effect(vars);
305 ExprKind::Loop(block, label, _, _) | ExprKind::Block(block, label) => {
306 self.visit_block_label(block, label);
308 ExprKind::Assign(bind, expr, _) => {
309 self.visit_assign(bind, expr);
311 ExprKind::AssignOp(_, bind, expr) => {
312 self.visit_assign(bind, expr);
313 self.visit_bin_op(bind, expr);
315 ExprKind::Field(expr, _) => {
316 self.visit_expr(expr);
317 if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) {
318 self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
321 ExprKind::Index(expr, index) => {
322 self.visit_expr(expr);
323 let mut vars = std::mem::take(&mut self.ret_vars);
324 self.visit_expr(index);
325 self.ret_vars.append(&mut vars);
327 if !is_array(self.ty_res.expr_ty(expr)) {
328 self.add_side_effect(self.ret_vars.clone());
329 } else if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) {
330 self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
333 ExprKind::Break(dest, Some(expr)) => {
334 self.visit_expr(expr);
335 if let Some(label) = dest.label {
338 .or_insert(Vec::new())
339 .append(&mut self.ret_vars);
341 self.contains_side_effect = true;
343 ExprKind::Ret(Some(expr)) => {
344 self.visit_expr(expr);
345 let vars = std::mem::take(&mut self.ret_vars);
346 self.add_side_effect(vars);
347 self.contains_side_effect = true;
349 ExprKind::Break(_, None) | ExprKind::Continue(_) | ExprKind::Ret(None) => {
350 self.contains_side_effect = true;
352 ExprKind::Struct(_, exprs, expr) => {
353 let mut ret_vars = exprs
356 self.visit_expr(field.expr);
357 std::mem::take(&mut self.ret_vars)
361 walk_list!(self, visit_expr, expr);
362 self.ret_vars.append(&mut ret_vars);
364 _ => walk_expr(self, ex),
368 fn visit_path(&mut self, path: &'tcx Path<'tcx>, _id: HirId) {
369 if let Res::Local(id) = path.res {
370 self.ret_vars.push((id, false));
375 impl<'tcx> SideEffectVisit<'tcx> {
376 fn visit_assign(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) {
377 // Just support array and tuple unwrapping for now.
379 // ex) `(a, b) = (c, d);`
380 // The graph would look like this:
384 // This would minimize the connection of the side-effect graph.
385 match (&lhs.kind, &rhs.kind) {
386 (ExprKind::Array(lhs), ExprKind::Array(rhs)) | (ExprKind::Tup(lhs), ExprKind::Tup(rhs)) => {
387 // if not, it is a compile error
388 debug_assert!(lhs.len() == rhs.len());
389 izip!(*lhs, *rhs).for_each(|(lhs, rhs)| self.visit_assign(lhs, rhs));
391 // in other assigns, we have to connect all each other
392 // because they can be connected somehow
394 self.visit_expr(lhs);
395 let lhs_vars = std::mem::take(&mut self.ret_vars);
396 self.visit_expr(rhs);
397 let rhs_vars = std::mem::take(&mut self.ret_vars);
398 self.connect_assign(&lhs_vars, &rhs_vars, false);
403 fn visit_block_label(&mut self, block: &'tcx Block<'tcx>, label: Option<Label>) {
404 self.visit_block(block);
405 let _ = label.and_then(|label| {
407 .remove(&label.ident)
408 .map(|mut break_vars| self.ret_vars.append(&mut break_vars))
412 fn visit_bin_op(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) {
413 self.visit_expr(lhs);
414 let mut ret_vars = std::mem::take(&mut self.ret_vars);
415 self.visit_expr(rhs);
416 self.ret_vars.append(&mut ret_vars);
418 // the binary operation between non primitive values are overloaded operators
419 // so they can have side-effects
420 if !is_primitive(self.ty_res.expr_ty(lhs)) || !is_primitive(self.ty_res.expr_ty(rhs)) {
421 self.ret_vars.iter().for_each(|id| {
422 self.has_side_effect.insert(id.0);
424 self.contains_side_effect = true;
428 fn visit_un_op(&mut self, op: UnOp, expr: &'tcx Expr<'tcx>) {
429 self.visit_expr(expr);
430 let ty = self.ty_res.expr_ty(expr);
431 // dereferencing a reference has no side-effect
432 if !is_primitive(ty) && !matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(..))) {
433 self.add_side_effect(self.ret_vars.clone());
436 if matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(_, _, Mutability::Mut))) {
437 self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
441 fn visit_pat_expr(&mut self, pat: &'tcx Pat<'tcx>, expr: &'tcx Expr<'tcx>, connect_self: bool) {
442 match (&pat.kind, &expr.kind) {
443 (PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) => {
444 self.ret_vars = izip!(*pats, *exprs)
445 .flat_map(|(pat, expr)| {
446 self.visit_pat_expr(pat, expr, connect_self);
447 std::mem::take(&mut self.ret_vars)
451 (PatKind::Slice(front_exprs, _, back_exprs), ExprKind::Array(exprs)) => {
452 let mut vars = izip!(*front_exprs, *exprs)
453 .flat_map(|(pat, expr)| {
454 self.visit_pat_expr(pat, expr, connect_self);
455 std::mem::take(&mut self.ret_vars)
458 self.ret_vars = izip!(back_exprs.iter().rev(), exprs.iter().rev())
459 .flat_map(|(pat, expr)| {
460 self.visit_pat_expr(pat, expr, connect_self);
461 std::mem::take(&mut self.ret_vars)
464 self.ret_vars.append(&mut vars);
467 let mut lhs_vars = Vec::new();
468 pat.each_binding(|_, id, _, _| lhs_vars.push((id, false)));
469 self.visit_expr(expr);
470 let rhs_vars = std::mem::take(&mut self.ret_vars);
471 self.connect_assign(&lhs_vars, &rhs_vars, connect_self);
472 self.ret_vars = rhs_vars;
477 fn visit_fn(&mut self, callee: &'tcx Expr<'tcx>, args: &'tcx [Expr<'tcx>]) {
478 self.visit_expr(callee);
479 let mut ret_vars = std::mem::take(&mut self.ret_vars);
480 self.add_side_effect(ret_vars.clone());
484 if let ExprKind::Path(QPath::Resolved(_, path)) = callee.kind;
485 if let Res::Def(..) = path.res;
486 if path.segments.len() == 1;
487 let ident = path.segments.last().unwrap().ident;
488 if ident == self.fn_ident;
490 izip!(self.params.clone(), args)
491 .for_each(|(pat, expr)| {
492 self.visit_pat_expr(pat, expr, true);
493 self.ret_vars.clear();
496 // This would set arguments used in closure that does not have side-effect.
497 // Closure itself can be detected whether there is a side-effect, but the
498 // value of variable that is holding closure can change.
499 // So, we just check the variables.
503 self.visit_expr(expr);
504 std::mem::take(&mut self.ret_vars)
509 self.has_side_effect.insert(id.0);
513 self.contains_side_effect = true;
517 self.ret_vars.append(&mut ret_vars);
520 fn visit_method_call(&mut self, path: &'tcx PathSegment<'tcx>, args: &'tcx [Expr<'tcx>]) {
523 if path.ident == self.fn_ident;
524 if let ExprKind::Path(QPath::Resolved(_, path)) = args.first().unwrap().kind;
525 if let Res::Local(..) = path.res;
526 let ident = path.segments.last().unwrap().ident;
527 if ident.name == kw::SelfLower;
529 izip!(self.params.clone(), args.iter())
530 .for_each(|(pat, expr)| {
531 self.visit_pat_expr(pat, expr, true);
532 self.ret_vars.clear();
538 self.visit_expr(expr);
539 std::mem::take(&mut self.ret_vars)
544 self.has_side_effect.insert(a.0);
548 self.contains_side_effect = true;
553 fn visit_if(&mut self, bind: &'tcx Expr<'tcx>, then_expr: &'tcx Expr<'tcx>, else_expr: Option<&'tcx Expr<'tcx>>) {
554 let contains_side_effect = self.contains_side_effect;
555 self.contains_side_effect = false;
556 self.visit_expr(bind);
557 let mut vars = std::mem::take(&mut self.ret_vars);
558 self.visit_expr(then_expr);
559 let mut then_vars = std::mem::take(&mut self.ret_vars);
560 walk_list!(self, visit_expr, else_expr);
561 if self.contains_side_effect {
562 self.add_side_effect(vars.clone());
564 self.contains_side_effect |= contains_side_effect;
565 self.ret_vars.append(&mut vars);
566 self.ret_vars.append(&mut then_vars);
569 fn visit_match(&mut self, expr: &'tcx Expr<'tcx>, arms: &'tcx [Arm<'tcx>]) {
570 self.visit_expr(expr);
571 let mut expr_vars = std::mem::take(&mut self.ret_vars);
575 let contains_side_effect = self.contains_side_effect;
576 self.contains_side_effect = false;
577 // this would visit `expr` multiple times
578 // but couldn't think of a better way
579 self.visit_pat_expr(arm.pat, expr, false);
580 let mut vars = std::mem::take(&mut self.ret_vars);
581 let _ = arm.guard.as_ref().map(|guard| {
582 self.visit_expr(match guard {
583 Guard::If(expr) | Guard::IfLet(_, expr) => expr,
585 vars.append(&mut self.ret_vars);
587 self.visit_expr(arm.body);
588 if self.contains_side_effect {
589 self.add_side_effect(vars.clone());
590 self.add_side_effect(expr_vars.clone());
592 self.contains_side_effect |= contains_side_effect;
593 vars.append(&mut self.ret_vars);
597 self.ret_vars.append(&mut expr_vars);
600 fn connect_assign(&mut self, lhs: &[(HirId, bool)], rhs: &[(HirId, bool)], connect_self: bool) {
601 // if mutable dereference is on assignment it can have side-effect
602 // (this can lead to parameter mutable dereference and change the original value)
603 // too hard to detect whether this value is from parameter, so this would all
604 // check mutable dereference assignment to side effect
605 lhs.iter().filter(|(_, b)| *b).for_each(|(id, _)| {
606 self.has_side_effect.insert(*id);
607 self.contains_side_effect = true;
610 // there is no connection
611 if lhs.is_empty() || rhs.is_empty() {
615 // by connected rhs in cycle, the connections would decrease
616 // from `n * m` to `n + m`
617 // where `n` and `m` are length of `lhs` and `rhs`.
619 // unwrap is possible since rhs is not empty
620 let rhs_first = rhs.first().unwrap();
621 for (id, _) in lhs.iter() {
622 if connect_self || *id != rhs_first.0 {
625 .or_insert_with(FxHashSet::default)
626 .insert(rhs_first.0);
630 let rhs = rhs.iter();
631 izip!(rhs.clone().cycle().skip(1), rhs).for_each(|(from, to)| {
632 if connect_self || from.0 != to.0 {
633 self.graph.entry(from.0).or_insert_with(FxHashSet::default).insert(to.0);
638 fn add_side_effect(&mut self, v: Vec<(HirId, bool)>) {
640 self.has_side_effect.insert(id);
641 self.contains_side_effect = true;