about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJason Newcomb <jsnewcomb@pm.me>2022-05-08 13:11:53 -0400
committerJason Newcomb <jsnewcomb@pm.me>2022-08-19 11:42:14 -0400
commitd95b67560c4cce38e07aed560abf7832eb5aeaa7 (patch)
treed37ac454957f6edf1f7a2e9b10708535f9717e70
parent477c16d45bb93cc92611b56614ce2b823bffab8d (diff)
downloadrust-d95b67560c4cce38e07aed560abf7832eb5aeaa7.tar.gz
rust-d95b67560c4cce38e07aed560abf7832eb5aeaa7.zip
Rework `only_used_in_recursion`
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_lints/src/only_used_in_recursion.rs828
-rw-r--r--tests/ui/only_used_in_recursion.rs133
-rw-r--r--tests/ui/only_used_in_recursion.stderr193
-rw-r--r--tests/ui/only_used_in_recursion2.rs91
-rw-r--r--tests/ui/only_used_in_recursion2.stderr63
6 files changed, 655 insertions, 655 deletions
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index dbea55a04d6..467051cfa68 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -860,7 +860,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(move || Box::new(manual_bits::ManualBits::new(msrv)));
     store.register_late_pass(|| Box::new(default_union_representation::DefaultUnionRepresentation));
     store.register_early_pass(|| Box::new(doc_link_with_quotes::DocLinkWithQuotes));
-    store.register_late_pass(|| Box::new(only_used_in_recursion::OnlyUsedInRecursion));
+    store.register_late_pass(|| Box::new(only_used_in_recursion::OnlyUsedInRecursion::default()));
     let allow_dbg_in_tests = conf.allow_dbg_in_tests;
     store.register_late_pass(move || Box::new(dbg_macro::DbgMacro::new(allow_dbg_in_tests)));
     let cargo_ignore_publish = conf.cargo_ignore_publish;
diff --git a/clippy_lints/src/only_used_in_recursion.rs b/clippy_lints/src/only_used_in_recursion.rs
index 413a740be25..a4f516df735 100644
--- a/clippy_lints/src/only_used_in_recursion.rs
+++ b/clippy_lints/src/only_used_in_recursion.rs
@@ -1,25 +1,16 @@
-use std::collections::VecDeque;
-
-use clippy_utils::diagnostics::span_lint_and_sugg;
-use clippy_utils::is_lint_allowed;
-use itertools::{izip, Itertools};
-use rustc_ast::{walk_list, Label, Mutability};
-use rustc_data_structures::fx::{FxHashMap, FxHashSet};
+use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::{get_expr_use_or_unification_node, get_parent_node, path_def_id, path_to_local, path_to_local_id};
+use core::cell::Cell;
+use rustc_data_structures::fx::FxHashMap;
 use rustc_errors::Applicability;
-use rustc_hir::def::{DefKind, Res};
 use rustc_hir::def_id::DefId;
-use rustc_hir::definitions::{DefPathData, DisambiguatedDefPathData};
-use rustc_hir::intravisit::{walk_expr, walk_stmt, FnKind, Visitor};
-use rustc_hir::{
-    Arm, Block, Body, Closure, Expr, ExprKind, Guard, HirId, ImplicitSelfKind, Let, Local, Pat, PatKind, Path,
-    PathSegment, QPath, Stmt, StmtKind, TyKind, UnOp,
-};
+use rustc_hir::hir_id::HirIdMap;
+use rustc_hir::{Body, Expr, ExprKind, HirId, ImplItem, ImplItemKind, Node, PatKind, TraitItem, TraitItemKind};
 use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::ty;
-use rustc_middle::ty::{Ty, TyCtxt, TypeckResults};
-use rustc_session::{declare_lint_pass, declare_tool_lint};
-use rustc_span::symbol::kw;
-use rustc_span::symbol::Ident;
+use rustc_middle::ty::subst::{GenericArgKind, SubstsRef};
+use rustc_middle::ty::{self, ConstKind};
+use rustc_session::{declare_tool_lint, impl_lint_pass};
+use rustc_span::symbol::{kw, Ident};
 use rustc_span::Span;
 
 declare_clippy_lint! {
@@ -92,569 +83,320 @@ declare_clippy_lint! {
     nursery,
     "arguments that is only used in recursion can be removed"
 }
-declare_lint_pass!(OnlyUsedInRecursion => [ONLY_USED_IN_RECURSION]);
-
-impl<'tcx> LateLintPass<'tcx> for OnlyUsedInRecursion {
-    fn check_fn(
-        &mut self,
-        cx: &LateContext<'tcx>,
-        kind: FnKind<'tcx>,
-        decl: &'tcx rustc_hir::FnDecl<'tcx>,
-        body: &'tcx Body<'tcx>,
-        _: Span,
-        id: HirId,
-    ) {
-        if is_lint_allowed(cx, ONLY_USED_IN_RECURSION, id) {
-            return;
-        }
-        if let FnKind::ItemFn(ident, ..) | FnKind::Method(ident, ..) = kind {
-            let def_id = id.owner.to_def_id();
-            let data = cx.tcx.def_path(def_id).data;
-
-            if data.len() > 1 {
-                match data.get(data.len() - 2) {
-                    Some(DisambiguatedDefPathData {
-                        data: DefPathData::Impl,
-                        disambiguator,
-                    }) if *disambiguator != 0 => return,
-                    _ => {},
-                }
-            }
-
-            let has_self = !matches!(decl.implicit_self, ImplicitSelfKind::None);
-
-            let ty_res = cx.typeck_results();
-            let param_span = body
-                .params
-                .iter()
-                .flat_map(|param| {
-                    let mut v = Vec::new();
-                    param.pat.each_binding(|_, hir_id, span, ident| {
-                        v.push((hir_id, span, ident));
-                    });
-                    v
-                })
-                .skip(if has_self { 1 } else { 0 })
-                .filter(|(_, _, ident)| !ident.name.as_str().starts_with('_'))
-                .collect_vec();
-
-            let params = body.params.iter().map(|param| param.pat).collect();
-
-            let mut visitor = SideEffectVisit {
-                graph: FxHashMap::default(),
-                has_side_effect: FxHashSet::default(),
-                ret_vars: Vec::new(),
-                contains_side_effect: false,
-                break_vars: FxHashMap::default(),
-                params,
-                fn_ident: ident,
-                fn_def_id: def_id,
-                is_method: matches!(kind, FnKind::Method(..)),
-                has_self,
-                ty_res,
-                tcx: cx.tcx,
-                visited_exprs: FxHashSet::default(),
-            };
-
-            visitor.visit_expr(&body.value);
-            let vars = std::mem::take(&mut visitor.ret_vars);
-            // this would set the return variables to side effect
-            visitor.add_side_effect(vars);
-
-            let mut queue = visitor.has_side_effect.iter().copied().collect::<VecDeque<_>>();
-
-            // a simple BFS to check all the variables that have side effect
-            while let Some(id) = queue.pop_front() {
-                if let Some(next) = visitor.graph.get(&id) {
-                    for i in next {
-                        if !visitor.has_side_effect.contains(i) {
-                            visitor.has_side_effect.insert(*i);
-                            queue.push_back(*i);
-                        }
-                    }
-                }
-            }
-
-            for (id, span, ident) in param_span {
-                // if the variable is not used in recursion, it would be marked as unused
-                if !visitor.has_side_effect.contains(&id) {
-                    let mut queue = VecDeque::new();
-                    let mut visited = FxHashSet::default();
-
-                    queue.push_back(id);
-
-                    // a simple BFS to check the graph can reach to itself
-                    // if it can't, it means the variable is never used in recursion
-                    while let Some(id) = queue.pop_front() {
-                        if let Some(next) = visitor.graph.get(&id) {
-                            for i in next {
-                                if !visited.contains(i) {
-                                    visited.insert(id);
-                                    queue.push_back(*i);
-                                }
-                            }
-                        }
-                    }
+impl_lint_pass!(OnlyUsedInRecursion => [ONLY_USED_IN_RECURSION]);
+
+#[derive(Clone, Copy)]
+enum FnKind {
+    Fn,
+    TraitFn,
+    // This is a hack. Ideally we would store a `SubstsRef<'tcx>` type here, but a lint pass must be `'static`.
+    // Substitutions are, however, interned. This allows us to store the pointer as a `usize` when comparing for
+    // equality.
+    ImplTraitFn(usize),
+}
 
-                    if visited.contains(&id) {
-                        span_lint_and_sugg(
-                            cx,
-                            ONLY_USED_IN_RECURSION,
-                            span,
-                            "parameter is only used in recursion",
-                            "if this is intentional, prefix with an underscore",
-                            format!("_{}", ident.name.as_str()),
-                            Applicability::MaybeIncorrect,
-                        );
-                    }
-                }
-            }
+struct Param {
+    /// The function this is a parameter for.
+    fn_id: DefId,
+    fn_kind: FnKind,
+    /// The index of this parameter.
+    idx: usize,
+    ident: Ident,
+    /// Whether this parameter should be linted. Set by `Params::flag_for_linting`.
+    apply_lint: Cell<bool>,
+    /// All the uses of this parameter.
+    uses: Vec<Usage>,
+}
+impl Param {
+    fn new(fn_id: DefId, fn_kind: FnKind, idx: usize, ident: Ident) -> Self {
+        Self {
+            fn_id,
+            fn_kind,
+            idx,
+            ident,
+            apply_lint: Cell::new(true),
+            uses: Vec::new(),
         }
     }
 }
 
-pub fn is_primitive(ty: Ty<'_>) -> bool {
-    let ty = ty.peel_refs();
-    ty.is_primitive() || ty.is_str()
+#[derive(Debug)]
+struct Usage {
+    span: Span,
+    idx: usize,
 }
-
-pub fn is_array(ty: Ty<'_>) -> bool {
-    let ty = ty.peel_refs();
-    ty.is_array() || ty.is_array_slice()
+impl Usage {
+    fn new(span: Span, idx: usize) -> Self {
+        Self { span, idx }
+    }
 }
 
-/// This builds the graph of side effect.
-/// The edge `a -> b` means if `a` has side effect, `b` will have side effect.
-///
-/// There are some example in following code:
-/// ```rust, ignore
-/// let b = 1;
-/// let a = b; // a -> b
-/// let (c, d) = (a, b); // c -> b, d -> b
-///
-/// let e = if a == 0 { // e -> a
-///     c // e -> c
-/// } else {
-///     d // e -> d
-/// };
-/// ```
-pub struct SideEffectVisit<'tcx> {
-    graph: FxHashMap<HirId, FxHashSet<HirId>>,
-    has_side_effect: FxHashSet<HirId>,
-    // bool for if the variable was dereferenced from mutable reference
-    ret_vars: Vec<(HirId, bool)>,
-    contains_side_effect: bool,
-    // break label
-    break_vars: FxHashMap<Ident, Vec<(HirId, bool)>>,
-    params: Vec<&'tcx Pat<'tcx>>,
-    fn_ident: Ident,
-    fn_def_id: DefId,
-    is_method: bool,
-    has_self: bool,
-    ty_res: &'tcx TypeckResults<'tcx>,
-    tcx: TyCtxt<'tcx>,
-    visited_exprs: FxHashSet<HirId>,
+/// The parameters being checked by the lint, indexed by both the parameter's `HirId` and the
+/// `DefId` of the function paired with the parameter's index.
+#[derive(Default)]
+struct Params {
+    params: Vec<Param>,
+    by_id: HirIdMap<usize>,
+    by_fn: FxHashMap<(DefId, usize), usize>,
 }
-
-impl<'tcx> Visitor<'tcx> for SideEffectVisit<'tcx> {
-    fn visit_stmt(&mut self, s: &'tcx Stmt<'tcx>) {
-        match s.kind {
-            StmtKind::Local(Local {
-                pat, init: Some(init), ..
-            }) => {
-                self.visit_pat_expr(pat, init, false);
-            },
-            StmtKind::Item(_) | StmtKind::Expr(_) | StmtKind::Semi(_) => {
-                walk_stmt(self, s);
-            },
-            StmtKind::Local(_) => {},
-        }
-        self.ret_vars.clear();
+impl Params {
+    fn insert(&mut self, param: Param, id: HirId) {
+        let idx = self.params.len();
+        self.by_id.insert(id, idx);
+        self.by_fn.insert((param.fn_id, param.idx), idx);
+        self.params.push(param);
     }
 
-    fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) {
-        if !self.visited_exprs.insert(ex.hir_id) {
-            return;
-        }
-        match ex.kind {
-            ExprKind::Array(exprs) | ExprKind::Tup(exprs) => {
-                self.ret_vars = exprs
-                    .iter()
-                    .flat_map(|expr| {
-                        self.visit_expr(expr);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect();
-            },
-            ExprKind::Call(callee, args) => self.visit_fn(callee, args),
-            ExprKind::MethodCall(path, args, _) => self.visit_method_call(path, args),
-            ExprKind::Binary(_, lhs, rhs) => {
-                self.visit_bin_op(lhs, rhs);
-            },
-            ExprKind::Unary(op, expr) => self.visit_un_op(op, expr),
-            ExprKind::Let(Let { pat, init, .. }) => self.visit_pat_expr(pat, init, false),
-            ExprKind::If(bind, then_expr, else_expr) => {
-                self.visit_if(bind, then_expr, else_expr);
-            },
-            ExprKind::Match(expr, arms, _) => self.visit_match(expr, arms),
-            // since analysing the closure is not easy, just set all variables in it to side-effect
-            ExprKind::Closure(&Closure { body, .. }) => {
-                let body = self.tcx.hir().body(body);
-                self.visit_body(body);
-                let vars = std::mem::take(&mut self.ret_vars);
-                self.add_side_effect(vars);
-            },
-            ExprKind::Loop(block, label, _, _) | ExprKind::Block(block, label) => {
-                self.visit_block_label(block, label);
-            },
-            ExprKind::Assign(bind, expr, _) => {
-                self.visit_assign(bind, expr);
-            },
-            ExprKind::AssignOp(_, bind, expr) => {
-                self.visit_assign(bind, expr);
-                self.visit_bin_op(bind, expr);
-            },
-            ExprKind::Field(expr, _) => {
-                self.visit_expr(expr);
-                if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) {
-                    self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
-                }
-            },
-            ExprKind::Index(expr, index) => {
-                self.visit_expr(expr);
-                let mut vars = std::mem::take(&mut self.ret_vars);
-                self.visit_expr(index);
-                self.ret_vars.append(&mut vars);
-
-                if !is_array(self.ty_res.expr_ty(expr)) {
-                    self.add_side_effect(self.ret_vars.clone());
-                } else if matches!(self.ty_res.expr_ty(expr).kind(), ty::Ref(_, _, Mutability::Mut)) {
-                    self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
-                }
-            },
-            ExprKind::Break(dest, Some(expr)) => {
-                self.visit_expr(expr);
-                if let Some(label) = dest.label {
-                    self.break_vars
-                        .entry(label.ident)
-                        .or_insert(Vec::new())
-                        .append(&mut self.ret_vars);
-                }
-                self.contains_side_effect = true;
-            },
-            ExprKind::Ret(Some(expr)) => {
-                self.visit_expr(expr);
-                let vars = std::mem::take(&mut self.ret_vars);
-                self.add_side_effect(vars);
-                self.contains_side_effect = true;
-            },
-            ExprKind::Break(_, None) | ExprKind::Continue(_) | ExprKind::Ret(None) => {
-                self.contains_side_effect = true;
-            },
-            ExprKind::Struct(_, exprs, expr) => {
-                let mut ret_vars = exprs
-                    .iter()
-                    .flat_map(|field| {
-                        self.visit_expr(field.expr);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect();
-
-                walk_list!(self, visit_expr, expr);
-                self.ret_vars.append(&mut ret_vars);
-            },
-            _ => walk_expr(self, ex),
+    fn remove_by_id(&mut self, id: HirId) {
+        if let Some(param) = self.get_by_id_mut(id) {
+            param.uses = Vec::new();
+            let key = (param.fn_id, param.idx);
+            self.by_fn.remove(&key);
+            self.by_id.remove(&id);
         }
     }
 
-    fn visit_path(&mut self, path: &'tcx Path<'tcx>, _id: HirId) {
-        if let Res::Local(id) = path.res {
-            self.ret_vars.push((id, false));
-        }
+    fn get_by_id_mut(&mut self, id: HirId) -> Option<&mut Param> {
+        self.params.get_mut(*self.by_id.get(&id)?)
     }
-}
 
-impl<'tcx> SideEffectVisit<'tcx> {
-    fn visit_assign(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) {
-        // Just support array and tuple unwrapping for now.
-        //
-        // ex) `(a, b) = (c, d);`
-        // The graph would look like this:
-        //   a -> c
-        //   b -> d
-        //
-        // This would minimize the connection of the side-effect graph.
-        match (&lhs.kind, &rhs.kind) {
-            (ExprKind::Array(lhs), ExprKind::Array(rhs)) | (ExprKind::Tup(lhs), ExprKind::Tup(rhs)) => {
-                // if not, it is a compile error
-                debug_assert!(lhs.len() == rhs.len());
-                izip!(*lhs, *rhs).for_each(|(lhs, rhs)| self.visit_assign(lhs, rhs));
-            },
-            // in other assigns, we have to connect all each other
-            // because they can be connected somehow
-            _ => {
-                self.visit_expr(lhs);
-                let lhs_vars = std::mem::take(&mut self.ret_vars);
-                self.visit_expr(rhs);
-                let rhs_vars = std::mem::take(&mut self.ret_vars);
-                self.connect_assign(&lhs_vars, &rhs_vars, false);
-            },
-        }
+    fn get_by_fn(&self, id: DefId, idx: usize) -> Option<&Param> {
+        self.params.get(*self.by_fn.get(&(id, idx))?)
     }
 
-    fn visit_block_label(&mut self, block: &'tcx Block<'tcx>, label: Option<Label>) {
-        self.visit_block(block);
-        let _ = label.and_then(|label| {
-            self.break_vars
-                .remove(&label.ident)
-                .map(|mut break_vars| self.ret_vars.append(&mut break_vars))
-        });
-    }
-
-    fn visit_bin_op(&mut self, lhs: &'tcx Expr<'tcx>, rhs: &'tcx Expr<'tcx>) {
-        self.visit_expr(lhs);
-        let mut ret_vars = std::mem::take(&mut self.ret_vars);
-        self.visit_expr(rhs);
-        self.ret_vars.append(&mut ret_vars);
-
-        // the binary operation between non primitive values are overloaded operators
-        // so they can have side-effects
-        if !is_primitive(self.ty_res.expr_ty(lhs)) || !is_primitive(self.ty_res.expr_ty(rhs)) {
-            self.ret_vars.iter().for_each(|id| {
-                self.has_side_effect.insert(id.0);
-            });
-            self.contains_side_effect = true;
-        }
+    fn clear(&mut self) {
+        self.params.clear();
+        self.by_id.clear();
+        self.by_fn.clear();
     }
 
-    fn visit_un_op(&mut self, op: UnOp, expr: &'tcx Expr<'tcx>) {
-        self.visit_expr(expr);
-        let ty = self.ty_res.expr_ty(expr);
-        // dereferencing a reference has no side-effect
-        if !is_primitive(ty) && !matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(..))) {
-            self.add_side_effect(self.ret_vars.clone());
-        }
-
-        if matches!((op, ty.kind()), (UnOp::Deref, ty::Ref(_, _, Mutability::Mut))) {
-            self.ret_vars.iter_mut().for_each(|(_, b)| *b = true);
+    /// Sets the `apply_lint` flag on each parameter.
+    fn flag_for_linting(&mut self) {
+        // Stores the list of parameters currently being resolved. Needed to avoid cycles.
+        let mut eval_stack = Vec::new();
+        for param in &self.params {
+            self.try_disable_lint_for_param(param, &mut eval_stack);
         }
     }
 
-    fn visit_pat_expr(&mut self, pat: &'tcx Pat<'tcx>, expr: &'tcx Expr<'tcx>, connect_self: bool) {
-        match (&pat.kind, &expr.kind) {
-            (PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) => {
-                self.ret_vars = izip!(*pats, *exprs)
-                    .flat_map(|(pat, expr)| {
-                        self.visit_pat_expr(pat, expr, connect_self);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect();
-            },
-            (PatKind::Slice(front_exprs, _, back_exprs), ExprKind::Array(exprs)) => {
-                let mut vars = izip!(*front_exprs, *exprs)
-                    .flat_map(|(pat, expr)| {
-                        self.visit_pat_expr(pat, expr, connect_self);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect();
-                self.ret_vars = izip!(back_exprs.iter().rev(), exprs.iter().rev())
-                    .flat_map(|(pat, expr)| {
-                        self.visit_pat_expr(pat, expr, connect_self);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect();
-                self.ret_vars.append(&mut vars);
-            },
-            _ => {
-                let mut lhs_vars = Vec::new();
-                pat.each_binding(|_, id, _, _| lhs_vars.push((id, false)));
-                self.visit_expr(expr);
-                let rhs_vars = std::mem::take(&mut self.ret_vars);
-                self.connect_assign(&lhs_vars, &rhs_vars, connect_self);
-                self.ret_vars = rhs_vars;
-            },
+    // Use by calling `flag_for_linting`.
+    fn try_disable_lint_for_param(&self, param: &Param, eval_stack: &mut Vec<usize>) -> bool {
+        if !param.apply_lint.get() {
+            true
+        } else if param.uses.is_empty() {
+            // Don't lint on unused parameters.
+            param.apply_lint.set(false);
+            true
+        } else if eval_stack.contains(&param.idx) {
+            // Already on the evaluation stack. Returning false will continue to evaluate other dependencies.
+            false
+        } else {
+            eval_stack.push(param.idx);
+            // Check all cases when used at a different parameter index.
+            // Needed to catch cases like: `fn f(x: u32, y: u32) { f(y, x) }`
+            for usage in param.uses.iter().filter(|u| u.idx != param.idx) {
+                if self
+                    .get_by_fn(param.fn_id, usage.idx)
+                    // If the parameter can't be found, then it's used for more than just recursion.
+                    .map_or(true, |p| self.try_disable_lint_for_param(p, eval_stack))
+                {
+                    param.apply_lint.set(false);
+                    eval_stack.pop();
+                    return true;
+                }
+            }
+            eval_stack.pop();
+            false
         }
     }
+}
 
-    fn visit_fn(&mut self, callee: &'tcx Expr<'tcx>, args: &'tcx [Expr<'tcx>]) {
-        self.visit_expr(callee);
-        let mut ret_vars = std::mem::take(&mut self.ret_vars);
-        self.add_side_effect(ret_vars.clone());
-
-        let mut is_recursive = false;
-
-        if_chain! {
-            if !self.has_self;
-            if let ExprKind::Path(QPath::Resolved(_, path)) = callee.kind;
-            if let Res::Def(DefKind::Fn, def_id) = path.res;
-            if self.fn_def_id == def_id;
-            then {
-                is_recursive = true;
-            }
-        }
+#[derive(Default)]
+pub struct OnlyUsedInRecursion {
+    /// Track the top-level body entered. Needed to delay reporting when entering nested bodies.
+    entered_body: Option<HirId>,
+    params: Params,
+}
 
-        if_chain! {
-            if !self.has_self && self.is_method;
-            if let ExprKind::Path(QPath::TypeRelative(ty, segment)) = callee.kind;
-            if segment.ident == self.fn_ident;
-            if let TyKind::Path(QPath::Resolved(_, path)) = ty.kind;
-            if let Res::SelfTy{ .. } = path.res;
-            then {
-                is_recursive = true;
-            }
+impl<'tcx> LateLintPass<'tcx> for OnlyUsedInRecursion {
+    fn check_body(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'tcx>) {
+        if body.value.span.from_expansion() {
+            return;
         }
-
-        if is_recursive {
-            izip!(self.params.clone(), args).for_each(|(pat, expr)| {
-                self.visit_pat_expr(pat, expr, true);
-                self.ret_vars.clear();
-            });
-        } else {
-            // This would set arguments used in closure that does not have side-effect.
-            // Closure itself can be detected whether there is a side-effect, but the
-            // value of variable that is holding closure can change.
-            // So, we just check the variables.
-            self.ret_vars = args
-                .iter()
-                .flat_map(|expr| {
-                    self.visit_expr(expr);
-                    std::mem::take(&mut self.ret_vars)
-                })
-                .collect_vec()
-                .into_iter()
-                .map(|id| {
-                    self.has_side_effect.insert(id.0);
-                    id
-                })
-                .collect();
-            self.contains_side_effect = true;
+        // `skip_params` is either `0` or `1` to skip the `self` parameter in trait functions.
+        // It can't be renamed, and it can't be removed without removing it from multiple functions.
+        let (fn_id, fn_kind, skip_params) = match get_parent_node(cx.tcx, body.value.hir_id) {
+            Some(Node::Item(i)) => (i.def_id.to_def_id(), FnKind::Fn, 0),
+            Some(Node::TraitItem(&TraitItem {
+                kind: TraitItemKind::Fn(ref sig, _),
+                def_id,
+                ..
+            })) => (
+                def_id.to_def_id(),
+                FnKind::TraitFn,
+                if sig.decl.implicit_self.has_implicit_self() {
+                    1
+                } else {
+                    0
+                },
+            ),
+            Some(Node::ImplItem(&ImplItem {
+                kind: ImplItemKind::Fn(ref sig, _),
+                def_id,
+                ..
+            })) => {
+                #[allow(trivial_casts)]
+                if let Some(Node::Item(item)) = get_parent_node(cx.tcx, cx.tcx.hir().local_def_id_to_hir_id(def_id))
+                    && let Some(trait_ref) = cx.tcx.impl_trait_ref(item.def_id)
+                    && let Some(trait_item_id) = cx.tcx.associated_item(def_id).trait_item_def_id
+                {
+                    (
+                        trait_item_id,
+                        FnKind::ImplTraitFn(cx.tcx.erase_regions(trait_ref.substs) as *const _ as usize),
+                        if sig.decl.implicit_self.has_implicit_self() {
+                            1
+                        } else {
+                            0
+                        },
+                    )
+                } else {
+                    (def_id.to_def_id(), FnKind::Fn, 0)
+                }
+            },
+            _ => return,
+        };
+        body.params
+            .iter()
+            .enumerate()
+            .skip(skip_params)
+            .filter_map(|(idx, p)| match p.pat.kind {
+                PatKind::Binding(_, id, ident, None) if !ident.as_str().starts_with('_') => {
+                    Some((id, Param::new(fn_id, fn_kind, idx, ident)))
+                },
+                _ => None,
+            })
+            .for_each(|(id, param)| self.params.insert(param, id));
+        if self.entered_body.is_none() {
+            self.entered_body = Some(body.value.hir_id);
         }
-
-        self.ret_vars.append(&mut ret_vars);
     }
 
-    fn visit_method_call(&mut self, path: &'tcx PathSegment<'tcx>, args: &'tcx [Expr<'tcx>]) {
-        if_chain! {
-            if self.is_method;
-            if path.ident == self.fn_ident;
-            if let ExprKind::Path(QPath::Resolved(_, path)) = args.first().unwrap().kind;
-            if let Res::Local(..) = path.res;
-            let ident = path.segments.last().unwrap().ident;
-            if ident.name == kw::SelfLower;
-            then {
-                izip!(self.params.clone(), args.iter())
-                    .for_each(|(pat, expr)| {
-                        self.visit_pat_expr(pat, expr, true);
-                        self.ret_vars.clear();
-                    });
-            } else {
-                self.ret_vars = args
-                    .iter()
-                    .flat_map(|expr| {
-                        self.visit_expr(expr);
-                        std::mem::take(&mut self.ret_vars)
-                    })
-                    .collect_vec()
-                    .into_iter()
-                    .map(|a| {
-                        self.has_side_effect.insert(a.0);
-                        a
-                    })
-                    .collect();
-                self.contains_side_effect = true;
+    fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) {
+        if let Some(id) = path_to_local(e)
+            && let Some(param) = self.params.get_by_id_mut(id)
+        {
+            let typeck = cx.typeck_results();
+            let span = e.span;
+            let mut e = e;
+            loop {
+                match get_expr_use_or_unification_node(cx.tcx, e) {
+                    None | Some((Node::Stmt(_), _)) => return,
+                    Some((Node::Expr(parent), child_id)) => match parent.kind {
+                        // Recursive call. Track which index the parameter is used in.
+                        ExprKind::Call(callee, args)
+                            if path_def_id(cx, callee).map_or(false, |id| {
+                                id == param.fn_id
+                                    && has_matching_substs(param.fn_kind, typeck.node_substs(callee.hir_id))
+                            }) =>
+                        {
+                            if let Some(idx) = args.iter().position(|arg| arg.hir_id == child_id) {
+                                param.uses.push(Usage::new(span, idx));
+                            }
+                            return;
+                        },
+                        ExprKind::MethodCall(_, args, _)
+                            if typeck.type_dependent_def_id(parent.hir_id).map_or(false, |id| {
+                                id == param.fn_id
+                                    && has_matching_substs(param.fn_kind, typeck.node_substs(parent.hir_id))
+                            }) =>
+                        {
+                            if let Some(idx) = args.iter().position(|arg| arg.hir_id == child_id) {
+                                param.uses.push(Usage::new(span, idx));
+                            }
+                            return;
+                        },
+                        // Assignment to a parameter is fine.
+                        ExprKind::Assign(lhs, _, _) | ExprKind::AssignOp(_, lhs, _) if lhs.hir_id == child_id => {
+                            return;
+                        },
+                        // Parameter update e.g. `x = x + 1`
+                        ExprKind::Assign(lhs, rhs, _) | ExprKind::AssignOp(_, lhs, rhs)
+                            if rhs.hir_id == child_id && path_to_local_id(lhs, id) =>
+                        {
+                            return;
+                        },
+                        // Side-effect free expressions. Walk to the parent expression.
+                        ExprKind::Binary(_, lhs, rhs)
+                            if typeck.expr_ty(lhs).is_primitive() && typeck.expr_ty(rhs).is_primitive() =>
+                        {
+                            e = parent;
+                            continue;
+                        },
+                        ExprKind::Unary(_, arg) if typeck.expr_ty(arg).is_primitive() => {
+                            e = parent;
+                            continue;
+                        },
+                        ExprKind::AddrOf(..) | ExprKind::Cast(..) => {
+                            e = parent;
+                            continue;
+                        },
+                        // Only allow field accesses without auto-deref
+                        ExprKind::Field(..) if typeck.adjustments().get(child_id).is_none() => {
+                            e = parent;
+                            continue
+                        }
+                        _ => (),
+                    },
+                    _ => (),
+                }
+                self.params.remove_by_id(id);
+                return;
             }
         }
     }
 
-    fn visit_if(&mut self, bind: &'tcx Expr<'tcx>, then_expr: &'tcx Expr<'tcx>, else_expr: Option<&'tcx Expr<'tcx>>) {
-        let contains_side_effect = self.contains_side_effect;
-        self.contains_side_effect = false;
-        self.visit_expr(bind);
-        let mut vars = std::mem::take(&mut self.ret_vars);
-        self.visit_expr(then_expr);
-        let mut then_vars = std::mem::take(&mut self.ret_vars);
-        walk_list!(self, visit_expr, else_expr);
-        if self.contains_side_effect {
-            self.add_side_effect(vars.clone());
-        }
-        self.contains_side_effect |= contains_side_effect;
-        self.ret_vars.append(&mut vars);
-        self.ret_vars.append(&mut then_vars);
-    }
-
-    fn visit_match(&mut self, expr: &'tcx Expr<'tcx>, arms: &'tcx [Arm<'tcx>]) {
-        self.visit_expr(expr);
-        let mut expr_vars = std::mem::take(&mut self.ret_vars);
-        self.ret_vars = arms
-            .iter()
-            .flat_map(|arm| {
-                let contains_side_effect = self.contains_side_effect;
-                self.contains_side_effect = false;
-                // this would visit `expr` multiple times
-                // but couldn't think of a better way
-                self.visit_pat_expr(arm.pat, expr, false);
-                let mut vars = std::mem::take(&mut self.ret_vars);
-                let _ = arm.guard.as_ref().map(|guard| {
-                    self.visit_expr(match guard {
-                        Guard::If(expr) | Guard::IfLet(Let { init: expr, .. }) => expr,
-                    });
-                    vars.append(&mut self.ret_vars);
-                });
-                self.visit_expr(arm.body);
-                if self.contains_side_effect {
-                    self.add_side_effect(vars.clone());
-                    self.add_side_effect(expr_vars.clone());
+    fn check_body_post(&mut self, cx: &LateContext<'tcx>, body: &'tcx Body<'tcx>) {
+        if self.entered_body == Some(body.value.hir_id) {
+            self.entered_body = None;
+            self.params.flag_for_linting();
+            for param in &self.params.params {
+                if param.apply_lint.get() {
+                    span_lint_and_then(
+                        cx,
+                        ONLY_USED_IN_RECURSION,
+                        param.ident.span,
+                        "parameter is only used in recursion",
+                        |diag| {
+                            if param.ident.name != kw::SelfLower {
+                                diag.span_suggestion(
+                                    param.ident.span,
+                                    "if this is intentional, prefix it with an underscore",
+                                    format!("_{}", param.ident.name),
+                                    Applicability::MaybeIncorrect,
+                                );
+                            }
+                            diag.span_note(
+                                param.uses.iter().map(|x| x.span).collect::<Vec<_>>(),
+                                "parameter used here",
+                            );
+                        },
+                    );
                 }
-                self.contains_side_effect |= contains_side_effect;
-                vars.append(&mut self.ret_vars);
-                vars
-            })
-            .collect();
-        self.ret_vars.append(&mut expr_vars);
-    }
-
-    fn connect_assign(&mut self, lhs: &[(HirId, bool)], rhs: &[(HirId, bool)], connect_self: bool) {
-        // if mutable dereference is on assignment it can have side-effect
-        // (this can lead to parameter mutable dereference and change the original value)
-        // too hard to detect whether this value is from parameter, so this would all
-        // check mutable dereference assignment to side effect
-        lhs.iter().filter(|(_, b)| *b).for_each(|(id, _)| {
-            self.has_side_effect.insert(*id);
-            self.contains_side_effect = true;
-        });
-
-        // there is no connection
-        if lhs.is_empty() || rhs.is_empty() {
-            return;
-        }
-
-        // by connected rhs in cycle, the connections would decrease
-        // from `n * m` to `n + m`
-        // where `n` and `m` are length of `lhs` and `rhs`.
-
-        // unwrap is possible since rhs is not empty
-        let rhs_first = rhs.first().unwrap();
-        for (id, _) in lhs.iter() {
-            if connect_self || *id != rhs_first.0 {
-                self.graph
-                    .entry(*id)
-                    .or_insert_with(FxHashSet::default)
-                    .insert(rhs_first.0);
             }
+            self.params.clear();
         }
-
-        let rhs = rhs.iter();
-        izip!(rhs.clone().cycle().skip(1), rhs).for_each(|(from, to)| {
-            if connect_self || from.0 != to.0 {
-                self.graph.entry(from.0).or_insert_with(FxHashSet::default).insert(to.0);
-            }
-        });
     }
+}
 
-    fn add_side_effect(&mut self, v: Vec<(HirId, bool)>) {
-        for (id, _) in v {
-            self.has_side_effect.insert(id);
-            self.contains_side_effect = true;
-        }
+fn has_matching_substs(kind: FnKind, substs: SubstsRef<'_>) -> bool {
+    match kind {
+        FnKind::Fn => true,
+        FnKind::TraitFn => substs.iter().enumerate().all(|(idx, subst)| match subst.unpack() {
+            GenericArgKind::Lifetime(_) => true,
+            GenericArgKind::Type(ty) => matches!(*ty.kind(), ty::Param(ty) if ty.index as usize == idx),
+            GenericArgKind::Const(c) => matches!(c.kind(), ConstKind::Param(c) if c.index as usize == idx),
+        }),
+        #[allow(trivial_casts)]
+        FnKind::ImplTraitFn(expected_substs) => substs as *const _ as usize == expected_substs,
     }
 }
diff --git a/tests/ui/only_used_in_recursion.rs b/tests/ui/only_used_in_recursion.rs
index 5768434f988..f71e8ead519 100644
--- a/tests/ui/only_used_in_recursion.rs
+++ b/tests/ui/only_used_in_recursion.rs
@@ -1,122 +1,113 @@
 #![warn(clippy::only_used_in_recursion)]
 
-fn simple(a: usize, b: usize) -> usize {
-    if a == 0 { 1 } else { simple(a - 1, b) }
+fn _simple(x: u32) -> u32 {
+    x
 }
 
-fn with_calc(a: usize, b: isize) -> usize {
-    if a == 0 { 1 } else { with_calc(a - 1, -b + 1) }
+fn _simple2(x: u32) -> u32 {
+    _simple(x)
 }
 
-fn tuple((a, b): (usize, usize)) -> usize {
-    if a == 0 { 1 } else { tuple((a - 1, b + 1)) }
+fn _one_unused(flag: u32, a: usize) -> usize {
+    if flag == 0 { 0 } else { _one_unused(flag - 1, a) }
 }
 
-fn let_tuple(a: usize, b: usize) -> usize {
-    let (c, d) = (a, b);
-    if c == 0 { 1 } else { let_tuple(c - 1, d + 1) }
+fn _two_unused(flag: u32, a: u32, b: i32) -> usize {
+    if flag == 0 { 0 } else { _two_unused(flag - 1, a, b) }
 }
 
-fn array([a, b]: [usize; 2]) -> usize {
-    if a == 0 { 1 } else { array([a - 1, b + 1]) }
-}
-
-fn index(a: usize, mut b: &[usize], c: usize) -> usize {
-    if a == 0 { 1 } else { index(a - 1, b, c + b[0]) }
-}
-
-fn break_(a: usize, mut b: usize, mut c: usize) -> usize {
-    let c = loop {
-        b += 1;
-        c += 1;
-        if c == 10 {
-            break b;
-        }
-    };
-
-    if a == 0 { 1 } else { break_(a - 1, c, c) }
+fn _with_calc(flag: u32, a: i64) -> usize {
+    if flag == 0 {
+        0
+    } else {
+        _with_calc(flag - 1, (-a + 10) * 5)
+    }
 }
 
-// this has a side effect
-fn mut_ref(a: usize, b: &mut usize) -> usize {
-    *b = 1;
-    if a == 0 { 1 } else { mut_ref(a - 1, b) }
+// Don't lint
+fn _used_with_flag(flag: u32, a: u32) -> usize {
+    if flag == 0 { 0 } else { _used_with_flag(flag ^ a, a - 1) }
 }
 
-fn mut_ref2(a: usize, b: &mut usize) -> usize {
-    let mut c = *b;
-    if a == 0 { 1 } else { mut_ref2(a - 1, &mut c) }
+fn _used_with_unused(flag: u32, a: i32, b: i32) -> usize {
+    if flag == 0 {
+        0
+    } else {
+        _used_with_unused(flag - 1, -a, a + b)
+    }
 }
 
-fn not_primitive(a: usize, b: String) -> usize {
-    if a == 0 { 1 } else { not_primitive(a - 1, b) }
+fn _codependent_unused(flag: u32, a: i32, b: i32) -> usize {
+    if flag == 0 {
+        0
+    } else {
+        _codependent_unused(flag - 1, a * b, a + b)
+    }
 }
 
-// this doesn't have a side effect,
-// but `String` is not primitive.
-fn not_primitive_op(a: usize, b: String, c: &str) -> usize {
-    if a == 1 { 1 } else { not_primitive_op(a, b + c, c) }
+fn _not_primitive(flag: u32, b: String) -> usize {
+    if flag == 0 { 0 } else { _not_primitive(flag - 1, b) }
 }
 
 struct A;
 
 impl A {
-    fn method(a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { A::method(a - 1, b - 1) }
+    fn _method(flag: usize, a: usize) -> usize {
+        if flag == 0 { 0 } else { Self::_method(flag - 1, a) }
     }
 
-    fn method2(&self, a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { self.method2(a - 1, b + 1) }
+    fn _method_self(&self, flag: usize, a: usize) -> usize {
+        if flag == 0 { 0 } else { self._method_self(flag - 1, a) }
     }
 }
 
 trait B {
-    fn hello(a: usize, b: usize) -> usize;
-
-    fn hello2(&self, a: usize, b: usize) -> usize;
+    fn method(flag: u32, a: usize) -> usize;
+    fn method_self(&self, flag: u32, a: usize) -> usize;
 }
 
 impl B for A {
-    fn hello(a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { A::hello(a - 1, b + 1) }
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { Self::method(flag - 1, a) }
     }
 
-    fn hello2(&self, a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { self.hello2(a - 1, b + 1) }
+    fn method_self(&self, flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { self.method_self(flag - 1, a) }
     }
 }
 
-trait C {
-    fn hello(a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { Self::hello(a - 1, b + 1) }
+impl B for () {
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { a }
     }
 
-    fn hello2(&self, a: usize, b: usize) -> usize {
-        if a == 0 { 1 } else { self.hello2(a - 1, b + 1) }
+    fn method_self(&self, flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { a }
     }
 }
 
-fn ignore(a: usize, _: usize) -> usize {
-    if a == 1 { 1 } else { ignore(a - 1, 0) }
-}
+impl B for u32 {
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { <() as B>::method(flag, a) }
+    }
 
-fn ignore2(a: usize, _b: usize) -> usize {
-    if a == 1 { 1 } else { ignore2(a - 1, _b) }
+    fn method_self(&self, flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { ().method_self(flag, a) }
+    }
 }
 
-fn f1(a: u32) -> u32 {
-    a
-}
+trait C {
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { Self::method(flag - 1, a) }
+    }
 
-fn f2(a: u32) -> u32 {
-    f1(a)
+    fn method_self(&self, flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { self.method_self(flag - 1, a) }
+    }
 }
 
-fn inner_fn(a: u32) -> u32 {
-    fn inner_fn(a: u32) -> u32 {
-        a
-    }
-    inner_fn(a)
+fn _ignore(flag: usize, _a: usize) -> usize {
+    if flag == 0 { 0 } else { _ignore(flag - 1, _a) }
 }
 
 fn main() {}
diff --git a/tests/ui/only_used_in_recursion.stderr b/tests/ui/only_used_in_recursion.stderr
index 6fe9361bf5f..74057ddcfda 100644
--- a/tests/ui/only_used_in_recursion.stderr
+++ b/tests/ui/only_used_in_recursion.stderr
@@ -1,82 +1,195 @@
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:3:21
+  --> $DIR/only_used_in_recursion.rs:11:27
    |
-LL | fn simple(a: usize, b: usize) -> usize {
-   |                     ^ help: if this is intentional, prefix with an underscore: `_b`
+LL | fn _one_unused(flag: u32, a: usize) -> usize {
+   |                           ^ help: if this is intentional, prefix it with an underscore: `_a`
    |
    = note: `-D clippy::only-used-in-recursion` implied by `-D warnings`
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:12:53
+   |
+LL |     if flag == 0 { 0 } else { _one_unused(flag - 1, a) }
+   |                                                     ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion.rs:15:27
+   |
+LL | fn _two_unused(flag: u32, a: u32, b: i32) -> usize {
+   |                           ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:16:53
+   |
+LL |     if flag == 0 { 0 } else { _two_unused(flag - 1, a, b) }
+   |                                                     ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion.rs:15:35
+   |
+LL | fn _two_unused(flag: u32, a: u32, b: i32) -> usize {
+   |                                   ^ help: if this is intentional, prefix it with an underscore: `_b`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:16:56
+   |
+LL |     if flag == 0 { 0 } else { _two_unused(flag - 1, a, b) }
+   |                                                        ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion.rs:19:26
+   |
+LL | fn _with_calc(flag: u32, a: i64) -> usize {
+   |                          ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:23:32
+   |
+LL |         _with_calc(flag - 1, (-a + 10) * 5)
+   |                                ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:7:24
+  --> $DIR/only_used_in_recursion.rs:32:33
+   |
+LL | fn _used_with_unused(flag: u32, a: i32, b: i32) -> usize {
+   |                                 ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:36:38
    |
-LL | fn with_calc(a: usize, b: isize) -> usize {
-   |                        ^ help: if this is intentional, prefix with an underscore: `_b`
+LL |         _used_with_unused(flag - 1, -a, a + b)
+   |                                      ^  ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:11:14
+  --> $DIR/only_used_in_recursion.rs:32:41
+   |
+LL | fn _used_with_unused(flag: u32, a: i32, b: i32) -> usize {
+   |                                         ^ help: if this is intentional, prefix it with an underscore: `_b`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:36:45
    |
-LL | fn tuple((a, b): (usize, usize)) -> usize {
-   |              ^ help: if this is intentional, prefix with an underscore: `_b`
+LL |         _used_with_unused(flag - 1, -a, a + b)
+   |                                             ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:15:24
+  --> $DIR/only_used_in_recursion.rs:40:35
    |
-LL | fn let_tuple(a: usize, b: usize) -> usize {
-   |                        ^ help: if this is intentional, prefix with an underscore: `_b`
+LL | fn _codependent_unused(flag: u32, a: i32, b: i32) -> usize {
+   |                                   ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:44:39
+   |
+LL |         _codependent_unused(flag - 1, a * b, a + b)
+   |                                       ^      ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:20:14
+  --> $DIR/only_used_in_recursion.rs:40:43
+   |
+LL | fn _codependent_unused(flag: u32, a: i32, b: i32) -> usize {
+   |                                           ^ help: if this is intentional, prefix it with an underscore: `_b`
    |
-LL | fn array([a, b]: [usize; 2]) -> usize {
-   |              ^ help: if this is intentional, prefix with an underscore: `_b`
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:44:43
+   |
+LL |         _codependent_unused(flag - 1, a * b, a + b)
+   |                                           ^      ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:24:20
+  --> $DIR/only_used_in_recursion.rs:48:30
+   |
+LL | fn _not_primitive(flag: u32, b: String) -> usize {
+   |                              ^ help: if this is intentional, prefix it with an underscore: `_b`
    |
-LL | fn index(a: usize, mut b: &[usize], c: usize) -> usize {
-   |                    ^^^^^ help: if this is intentional, prefix with an underscore: `_b`
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:49:56
+   |
+LL |     if flag == 0 { 0 } else { _not_primitive(flag - 1, b) }
+   |                                                        ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:24:37
+  --> $DIR/only_used_in_recursion.rs:55:29
+   |
+LL |     fn _method(flag: usize, a: usize) -> usize {
+   |                             ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:56:59
    |
-LL | fn index(a: usize, mut b: &[usize], c: usize) -> usize {
-   |                                     ^ help: if this is intentional, prefix with an underscore: `_c`
+LL |         if flag == 0 { 0 } else { Self::_method(flag - 1, a) }
+   |                                                           ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:28:21
+  --> $DIR/only_used_in_recursion.rs:59:22
+   |
+LL |     fn _method_self(&self, flag: usize, a: usize) -> usize {
+   |                      ^^^^
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:60:35
    |
-LL | fn break_(a: usize, mut b: usize, mut c: usize) -> usize {
-   |                     ^^^^^ help: if this is intentional, prefix with an underscore: `_b`
+LL |         if flag == 0 { 0 } else { self._method_self(flag - 1, a) }
+   |                                   ^^^^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:46:23
+  --> $DIR/only_used_in_recursion.rs:59:41
    |
-LL | fn mut_ref2(a: usize, b: &mut usize) -> usize {
-   |                       ^ help: if this is intentional, prefix with an underscore: `_b`
+LL |     fn _method_self(&self, flag: usize, a: usize) -> usize {
+   |                                         ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:60:63
+   |
+LL |         if flag == 0 { 0 } else { self._method_self(flag - 1, a) }
+   |                                                               ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:51:28
+  --> $DIR/only_used_in_recursion.rs:70:26
+   |
+LL |     fn method(flag: u32, a: usize) -> usize {
+   |                          ^ help: if this is intentional, prefix it with an underscore: `_a`
    |
-LL | fn not_primitive(a: usize, b: String) -> usize {
-   |                            ^ help: if this is intentional, prefix with an underscore: `_b`
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:71:58
+   |
+LL |         if flag == 0 { 0 } else { Self::method(flag - 1, a) }
+   |                                                          ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:68:33
+  --> $DIR/only_used_in_recursion.rs:74:38
+   |
+LL |     fn method_self(&self, flag: u32, a: usize) -> usize {
+   |                                      ^ help: if this is intentional, prefix it with an underscore: `_a`
    |
-LL |     fn method2(&self, a: usize, b: usize) -> usize {
-   |                                 ^ help: if this is intentional, prefix with an underscore: `_b`
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:75:62
+   |
+LL |         if flag == 0 { 0 } else { self.method_self(flag - 1, a) }
+   |                                                              ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:90:24
+  --> $DIR/only_used_in_recursion.rs:100:26
+   |
+LL |     fn method(flag: u32, a: usize) -> usize {
+   |                          ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:101:58
    |
-LL |     fn hello(a: usize, b: usize) -> usize {
-   |                        ^ help: if this is intentional, prefix with an underscore: `_b`
+LL |         if flag == 0 { 0 } else { Self::method(flag - 1, a) }
+   |                                                          ^
 
 error: parameter is only used in recursion
-  --> $DIR/only_used_in_recursion.rs:94:32
+  --> $DIR/only_used_in_recursion.rs:104:38
+   |
+LL |     fn method_self(&self, flag: u32, a: usize) -> usize {
+   |                                      ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion.rs:105:62
    |
-LL |     fn hello2(&self, a: usize, b: usize) -> usize {
-   |                                ^ help: if this is intentional, prefix with an underscore: `_b`
+LL |         if flag == 0 { 0 } else { self.method_self(flag - 1, a) }
+   |                                                              ^
 
-error: aborting due to 13 previous errors
+error: aborting due to 16 previous errors
 
diff --git a/tests/ui/only_used_in_recursion2.rs b/tests/ui/only_used_in_recursion2.rs
new file mode 100644
index 00000000000..45dd0553f58
--- /dev/null
+++ b/tests/ui/only_used_in_recursion2.rs
@@ -0,0 +1,91 @@
+#![warn(clippy::only_used_in_recursion)]
+
+fn _with_inner(flag: u32, a: u32, b: u32) -> usize {
+    fn inner(flag: u32, a: u32) -> u32 {
+        if flag == 0 { 0 } else { inner(flag, a) }
+    }
+
+    let x = inner(flag, a);
+    if flag == 0 { 0 } else { _with_inner(flag, a, b + x) }
+}
+
+fn _with_closure(a: Option<u32>, b: u32, f: impl Fn(u32, u32) -> Option<u32>) -> u32 {
+    if let Some(x) = a.and_then(|x| f(x, x)) {
+        _with_closure(Some(x), b, f)
+    } else {
+        0
+    }
+}
+
+// Issue #8560
+trait D {
+    fn foo(&mut self, arg: u32) -> u32;
+}
+
+mod m {
+    pub struct S(u32);
+    impl S {
+        pub fn foo(&mut self, arg: u32) -> u32 {
+            arg + self.0
+        }
+    }
+}
+
+impl D for m::S {
+    fn foo(&mut self, arg: u32) -> u32 {
+        self.foo(arg)
+    }
+}
+
+// Issue #8782
+fn only_let(x: u32) {
+    let y = 10u32;
+    let _z = x * y;
+}
+
+trait E<T: E<()>> {
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 {
+            0
+        } else {
+            <T as E<()>>::method(flag - 1, a)
+        }
+    }
+}
+
+impl E<()> for () {
+    fn method(flag: u32, a: usize) -> usize {
+        if flag == 0 { 0 } else { a }
+    }
+}
+
+fn overwritten_param(flag: u32, mut a: usize) -> usize {
+    if flag == 0 {
+        return 0;
+    } else if flag > 5 {
+        a += flag as usize;
+    } else {
+        a = 5;
+    }
+    overwritten_param(flag, a)
+}
+
+fn field_direct(flag: u32, mut a: (usize,)) -> usize {
+    if flag == 0 {
+        0
+    } else {
+        a.0 += 5;
+        field_direct(flag - 1, a)
+    }
+}
+
+fn field_deref(flag: u32, a: &mut Box<(usize,)>) -> usize {
+    if flag == 0 {
+        0
+    } else {
+        a.0 += 5;
+        field_deref(flag - 1, a)
+    }
+}
+
+fn main() {}
diff --git a/tests/ui/only_used_in_recursion2.stderr b/tests/ui/only_used_in_recursion2.stderr
new file mode 100644
index 00000000000..23f6ffd30c9
--- /dev/null
+++ b/tests/ui/only_used_in_recursion2.stderr
@@ -0,0 +1,63 @@
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion2.rs:3:35
+   |
+LL | fn _with_inner(flag: u32, a: u32, b: u32) -> usize {
+   |                                   ^ help: if this is intentional, prefix it with an underscore: `_b`
+   |
+   = note: `-D clippy::only-used-in-recursion` implied by `-D warnings`
+note: parameter used here
+  --> $DIR/only_used_in_recursion2.rs:9:52
+   |
+LL |     if flag == 0 { 0 } else { _with_inner(flag, a, b + x) }
+   |                                                    ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion2.rs:4:25
+   |
+LL |     fn inner(flag: u32, a: u32) -> u32 {
+   |                         ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion2.rs:5:47
+   |
+LL |         if flag == 0 { 0 } else { inner(flag, a) }
+   |                                               ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion2.rs:12:34
+   |
+LL | fn _with_closure(a: Option<u32>, b: u32, f: impl Fn(u32, u32) -> Option<u32>) -> u32 {
+   |                                  ^ help: if this is intentional, prefix it with an underscore: `_b`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion2.rs:14:32
+   |
+LL |         _with_closure(Some(x), b, f)
+   |                                ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion2.rs:62:37
+   |
+LL | fn overwritten_param(flag: u32, mut a: usize) -> usize {
+   |                                     ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion2.rs:70:29
+   |
+LL |     overwritten_param(flag, a)
+   |                             ^
+
+error: parameter is only used in recursion
+  --> $DIR/only_used_in_recursion2.rs:73:32
+   |
+LL | fn field_direct(flag: u32, mut a: (usize,)) -> usize {
+   |                                ^ help: if this is intentional, prefix it with an underscore: `_a`
+   |
+note: parameter used here
+  --> $DIR/only_used_in_recursion2.rs:78:32
+   |
+LL |         field_direct(flag - 1, a)
+   |                                ^
+
+error: aborting due to 5 previous errors
+