Consider nested lifetimes in mut_from_ref (#14471)

fixes #7749.
This issue proposes searching for `DerefMut` impls, which is not done
here: every lifetime parameter (aka `<'a>`) is the input types is
considered to be potentially mutable, and thus deactivates the lint.

changelog: [`mut_from_ref`]: Fixes false positive, where lifetimes
nested in the type (e.g. `Box<&'a mut T>`) were not considered.
This commit is contained in:
Alejandra González 2025-04-14 23:43:42 +00:00 committed by GitHub
commit d3267e9230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 113 additions and 29 deletions

View file

@ -498,29 +498,33 @@ fn check_fn_args<'cx, 'tcx: 'cx>(
}
fn check_mut_from_ref<'tcx>(cx: &LateContext<'tcx>, sig: &FnSig<'_>, body: Option<&Body<'tcx>>) {
if let FnRetTy::Return(ty) = sig.decl.output
&& let Some((out, Mutability::Mut, _)) = get_ref_lm(ty)
{
let FnRetTy::Return(ty) = sig.decl.output else { return };
for (out, mutability, out_span) in get_lifetimes(ty) {
if mutability != Some(Mutability::Mut) {
continue;
}
let out_region = cx.tcx.named_bound_var(out.hir_id);
let args: Option<Vec<_>> = sig
// `None` if one of the types contains `&'a mut T` or `T<'a>`.
// Else, contains all the locations of `&'a T` types.
let args_immut_refs: Option<Vec<Span>> = sig
.decl
.inputs
.iter()
.filter_map(get_ref_lm)
.flat_map(get_lifetimes)
.filter(|&(lt, _, _)| cx.tcx.named_bound_var(lt.hir_id) == out_region)
.map(|(_, mutability, span)| (mutability == Mutability::Not).then_some(span))
.map(|(_, mutability, span)| (mutability == Some(Mutability::Not)).then_some(span))
.collect();
if let Some(args) = args
&& !args.is_empty()
if let Some(args_immut_refs) = args_immut_refs
&& !args_immut_refs.is_empty()
&& body.is_none_or(|body| sig.header.is_unsafe() || contains_unsafe_block(cx, body.value))
{
span_lint_and_then(
cx,
MUT_FROM_REF,
ty.span,
out_span,
"mutable borrow from immutable input(s)",
|diag| {
let ms = MultiSpan::from_spans(args);
let ms = MultiSpan::from_spans(args_immut_refs);
diag.span_note(ms, "immutable borrow here");
},
);
@ -686,12 +690,36 @@ fn matches_preds<'tcx>(
})
}
fn get_ref_lm<'tcx>(ty: &'tcx hir::Ty<'tcx>) -> Option<(&'tcx Lifetime, Mutability, Span)> {
if let TyKind::Ref(lt, ref m) = ty.kind {
Some((lt, m.mutbl, ty.span))
} else {
None
struct LifetimeVisitor<'tcx> {
result: Vec<(&'tcx Lifetime, Option<Mutability>, Span)>,
}
impl<'tcx> Visitor<'tcx> for LifetimeVisitor<'tcx> {
fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) {
if let TyKind::Ref(lt, ref m) = ty.kind {
self.result.push((lt, Some(m.mutbl), ty.span));
}
hir::intravisit::walk_ty(self, ty);
}
fn visit_generic_arg(&mut self, generic_arg: &'tcx GenericArg<'tcx>) {
if let GenericArg::Lifetime(lt) = generic_arg {
self.result.push((lt, None, generic_arg.span()));
}
hir::intravisit::walk_generic_arg(self, generic_arg);
}
}
/// Visit `ty` and collect the all the lifetimes appearing in it, implicit or not.
///
/// The second field of the vector's elements indicate if the lifetime is attached to a
/// shared reference, a mutable reference, or neither.
fn get_lifetimes<'tcx>(ty: &'tcx hir::Ty<'tcx>) -> Vec<(&'tcx Lifetime, Option<Mutability>, Span)> {
use hir::intravisit::VisitorExt as _;
let mut visitor = LifetimeVisitor { result: Vec::new() };
visitor.visit_ty_unambig(ty);
visitor.result
}
fn is_null_path(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {