| use clippy_utils::diagnostics::span_lint_and_then; | |
| use clippy_utils::get_expr_use_or_unification_node; | |
| use clippy_utils::res::{MaybeQPath, MaybeResPath}; | |
| use core::cell::Cell; | |
| use rustc_data_structures::fx::FxHashMap; | |
| use rustc_errors::Applicability; | |
| use rustc_hir::def_id::DefId; | |
| use rustc_hir::hir_id::HirIdMap; | |
| use rustc_hir::{ | |
| Body, Expr, ExprKind, HirId, ImplItem, ImplItemImplKind, ImplItemKind, Node, PatKind, TraitItem, TraitItemKind, | |
| }; | |
| use rustc_lint::{LateContext, LateLintPass}; | |
| use rustc_middle::ty::{self, ConstKind, GenericArgKind, GenericArgsRef}; | |
| use rustc_session::impl_lint_pass; | |
| use rustc_span::Span; | |
| use rustc_span::symbol::{Ident, kw}; | |
| use std::iter; | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for arguments that are only used in recursion with no side-effects. | |
| /// | |
| /// ### Why is this bad? | |
| /// It could contain a useless calculation and can make function simpler. | |
| /// | |
| /// The arguments can be involved in calculations and assignments but as long as | |
| /// the calculations have no side-effects (function calls or mutating dereference) | |
| /// and the assigned variables are also only in recursion, it is useless. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// fn f(a: usize, b: usize) -> usize { | |
| /// if a == 0 { | |
| /// 1 | |
| /// } else { | |
| /// f(a - 1, b + 1) | |
| /// } | |
| /// } | |
| /// # fn main() { | |
| /// # print!("{}", f(1, 1)); | |
| /// # } | |
| /// ``` | |
| /// Use instead: | |
| /// ```no_run | |
| /// fn f(a: usize) -> usize { | |
| /// if a == 0 { | |
| /// 1 | |
| /// } else { | |
| /// f(a - 1) | |
| /// } | |
| /// } | |
| /// # fn main() { | |
| /// # print!("{}", f(1)); | |
| /// # } | |
| /// ``` | |
| /// | |
| /// ### Known problems | |
| /// Too many code paths in the linting code are currently untested and prone to produce false | |
| /// positives or are prone to have performance implications. | |
| /// | |
| /// In some cases, this would not catch all useless arguments. | |
| /// | |
| /// ```no_run | |
| /// fn foo(a: usize, b: usize) -> usize { | |
| /// let f = |x| x + 1; | |
| /// | |
| /// if a == 0 { | |
| /// 1 | |
| /// } else { | |
| /// foo(a - 1, f(b)) | |
| /// } | |
| /// } | |
| /// ``` | |
| /// | |
| /// For example, the argument `b` is only used in recursion, but the lint would not catch it. | |
| /// | |
| /// List of some examples that can not be caught: | |
| /// - binary operation of non-primitive types | |
| /// - closure usage | |
| /// - some `break` relative operations | |
| /// - struct pattern binding | |
| /// | |
| /// Also, when you recurse the function name with path segments, it is not possible to detect. | |
| #[clippy::version = "1.61.0"] | |
| pub ONLY_USED_IN_RECURSION, | |
| complexity, | |
| "arguments that is only used in recursion can be removed" | |
| } | |
| declare_clippy_lint! { | |
| /// ### What it does | |
| /// Checks for `self` receiver that is only used in recursion with no side-effects. | |
| /// | |
| /// ### Why is this bad? | |
| /// | |
| /// It may be possible to remove the `self` argument, allowing the function to be | |
| /// used without an object of type `Self`. | |
| /// | |
| /// ### Example | |
| /// ```no_run | |
| /// struct Foo; | |
| /// impl Foo { | |
| /// fn f(&self, n: u32) -> u32 { | |
| /// if n == 0 { | |
| /// 1 | |
| /// } else { | |
| /// n * self.f(n - 1) | |
| /// } | |
| /// } | |
| /// } | |
| /// # fn main() { | |
| /// # print!("{}", Foo.f(10)); | |
| /// # } | |
| /// ``` | |
| /// Use instead: | |
| /// ```no_run | |
| /// struct Foo; | |
| /// impl Foo { | |
| /// fn f(n: u32) -> u32 { | |
| /// if n == 0 { | |
| /// 1 | |
| /// } else { | |
| /// n * Self::f(n - 1) | |
| /// } | |
| /// } | |
| /// } | |
| /// # fn main() { | |
| /// # print!("{}", Foo::f(10)); | |
| /// # } | |
| /// ``` | |
| /// | |
| /// ### Known problems | |
| /// Too many code paths in the linting code are currently untested and prone to produce false | |
| /// positives or are prone to have performance implications. | |
| /// | |
| /// In some cases, this would not catch all useless arguments. | |
| /// | |
| /// ```no_run | |
| /// struct Foo; | |
| /// impl Foo { | |
| /// fn foo(&self, a: usize) -> usize { | |
| /// let f = |x| x; | |
| /// | |
| /// if a == 0 { | |
| /// 1 | |
| /// } else { | |
| /// f(self).foo(a) | |
| /// } | |
| /// } | |
| /// } | |
| /// ``` | |
| /// | |
| /// For example, here `self` is only used in recursion, but the lint would not catch it. | |
| /// | |
| /// List of some examples that can not be caught: | |
| /// - binary operation of non-primitive types | |
| /// - closure usage | |
| /// - some `break` relative operations | |
| /// - struct pattern binding | |
| /// | |
| /// Also, when you recurse the function name with path segments, it is not possible to detect. | |
| #[clippy::version = "1.92.0"] | |
| pub SELF_ONLY_USED_IN_RECURSION, | |
| pedantic, | |
| "self receiver only used to recursively call method can be removed" | |
| } | |
| impl_lint_pass!(OnlyUsedInRecursion => [ | |
| ONLY_USED_IN_RECURSION, | |
| SELF_ONLY_USED_IN_RECURSION, | |
| ]); | |
| #[derive(Clone, Copy)] | |
| enum FnKind { | |
| Fn, | |
| TraitFn, | |
| // This is a hack. Ideally we would store a `GenericArgsRef<'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), | |
| } | |
| 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(), | |
| } | |
| } | |
| } | |
| #[derive(Debug)] | |
| struct Usage { | |
| span: Span, | |
| idx: usize, | |
| } | |
| impl Usage { | |
| fn new(span: Span, idx: usize) -> Self { | |
| Self { span, idx } | |
| } | |
| } | |
| /// 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)] | |
| #[expect(clippy::struct_field_names)] | |
| struct Params { | |
| params: Vec<Param>, | |
| by_id: HirIdMap<usize>, | |
| by_fn: FxHashMap<(DefId, usize), usize>, | |
| } | |
| 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 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); | |
| // FIXME(rust/#120456) - is `swap_remove` correct? | |
| self.by_id.swap_remove(&id); | |
| } | |
| } | |
| fn get_by_id_mut(&mut self, id: HirId) -> Option<&mut Param> { | |
| self.params.get_mut(*self.by_id.get(&id)?) | |
| } | |
| fn get_by_fn(&self, id: DefId, idx: usize) -> Option<&Param> { | |
| self.params.get(*self.by_fn.get(&(id, idx))?) | |
| } | |
| fn clear(&mut self) { | |
| self.params.clear(); | |
| self.by_id.clear(); | |
| self.by_fn.clear(); | |
| } | |
| /// Sets the `apply_lint` flag on each parameter. | |
| fn flag_for_linting(&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); | |
| } | |
| } | |
| // 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(¶m.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. | |
| .is_none_or(|p| self.try_disable_lint_for_param(p, eval_stack)) | |
| { | |
| param.apply_lint.set(false); | |
| eval_stack.pop(); | |
| return true; | |
| } | |
| } | |
| eval_stack.pop(); | |
| false | |
| } | |
| } | |
| } | |
| #[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, | |
| } | |
| impl<'tcx> LateLintPass<'tcx> for OnlyUsedInRecursion { | |
| fn check_body(&mut self, cx: &LateContext<'tcx>, body: &Body<'tcx>) { | |
| if body.value.span.from_expansion() { | |
| return; | |
| } | |
| // `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 cx.tcx.parent_hir_node(body.value.hir_id) { | |
| Node::Item(i) => (i.owner_id.to_def_id(), FnKind::Fn, 0), | |
| Node::TraitItem(&TraitItem { | |
| kind: TraitItemKind::Fn(ref sig, _), | |
| owner_id, | |
| .. | |
| }) => ( | |
| owner_id.to_def_id(), | |
| FnKind::TraitFn, | |
| usize::from(sig.decl.implicit_self.has_implicit_self()), | |
| ), | |
| Node::ImplItem(&ImplItem { | |
| kind: ImplItemKind::Fn(ref sig, _), | |
| owner_id, | |
| impl_kind, | |
| .. | |
| }) => { | |
| if let ImplItemImplKind::Trait { trait_item_def_id, .. } = impl_kind | |
| && let Ok(trait_item_id) = trait_item_def_id | |
| { | |
| let impl_id = cx.tcx.local_parent(owner_id.def_id); | |
| let trait_ref = cx.tcx.impl_trait_ref(impl_id).instantiate_identity(); | |
| ( | |
| trait_item_id, | |
| FnKind::ImplTraitFn( | |
| std::ptr::from_ref(cx.tcx.erase_and_anonymize_regions(trait_ref.args)) as usize | |
| ), | |
| usize::from(sig.decl.implicit_self.has_implicit_self()), | |
| ) | |
| } else { | |
| (owner_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); | |
| } | |
| } | |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) { | |
| if let Some(id) = e.res_local_id() | |
| && 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 callee.res(cx).opt_def_id().is_some_and(|id| { | |
| id == param.fn_id && has_matching_args(param.fn_kind, typeck.node_args(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(_, receiver, args, _) | |
| if typeck.type_dependent_def_id(parent.hir_id).is_some_and(|id| { | |
| id == param.fn_id && has_matching_args(param.fn_kind, typeck.node_args(parent.hir_id)) | |
| }) => | |
| { | |
| if let Some(idx) = iter::once(receiver).chain(args).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 && lhs.res_local_id() == Some(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 check_body_post(&mut self, cx: &LateContext<'tcx>, body: &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() { | |
| if param.ident.name == kw::SelfLower { | |
| span_lint_and_then( | |
| cx, | |
| SELF_ONLY_USED_IN_RECURSION, | |
| param.ident.span, | |
| "`self` is only used in recursion", | |
| |diag| { | |
| diag.span_note( | |
| param.uses.iter().map(|x| x.span).collect::<Vec<_>>(), | |
| "`self` used here", | |
| ); | |
| }, | |
| ); | |
| } else { | |
| span_lint_and_then( | |
| cx, | |
| ONLY_USED_IN_RECURSION, | |
| param.ident.span, | |
| "parameter is only used in recursion", | |
| |diag| { | |
| 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.params.clear(); | |
| } | |
| } | |
| } | |
| fn has_matching_args(kind: FnKind, args: GenericArgsRef<'_>) -> bool { | |
| match kind { | |
| FnKind::Fn => true, | |
| FnKind::TraitFn => args.iter().enumerate().all(|(idx, subst)| match subst.kind() { | |
| 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), | |
| }), | |
| FnKind::ImplTraitFn(expected_args) => std::ptr::from_ref(args) as usize == expected_args, | |
| } | |
| } |