Skip to content

Commit d1cab08

Browse files
committed
Fix shadow_unrelated's behaviour with closures
Fixes #10780
1 parent f58088b commit d1cab08

File tree

3 files changed

+89
-9
lines changed

3 files changed

+89
-9
lines changed

clippy_lints/src/shadow.rs

+50-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use std::ops::ControlFlow;
2+
13
use clippy_utils::diagnostics::span_lint_and_then;
4+
use clippy_utils::path_to_local_id;
25
use clippy_utils::source::snippet;
3-
use clippy_utils::visitors::is_local_used;
6+
use clippy_utils::visitors::{Descend, Visitable, for_each_expr};
47
use rustc_data_structures::fx::FxHashMap;
58
use rustc_hir::def::Res;
69
use rustc_hir::def_id::LocalDefId;
@@ -175,17 +178,39 @@ fn is_shadow(cx: &LateContext<'_>, owner: LocalDefId, first: ItemLocalId, second
175178
false
176179
}
177180

181+
/// Checks if the given local is used, except for in child expression of `except`.
182+
///
183+
/// This is a version of [`is_local_used`](clippy_utils::visitors::is_local_used), used to
184+
/// implement the fix for <https://github.com/rust-lang/rust-clippy/issues/10780>.
185+
pub fn is_local_used_except<'tcx>(
186+
cx: &LateContext<'tcx>,
187+
visitable: impl Visitable<'tcx>,
188+
id: HirId,
189+
except: Option<HirId>,
190+
) -> bool {
191+
for_each_expr(cx, visitable, |e| {
192+
if except.is_some_and(|it| it == e.hir_id) {
193+
ControlFlow::Continue(Descend::No)
194+
} else if path_to_local_id(e, id) {
195+
ControlFlow::Break(())
196+
} else {
197+
ControlFlow::Continue(Descend::Yes)
198+
}
199+
})
200+
.is_some()
201+
}
202+
178203
fn lint_shadow(cx: &LateContext<'_>, pat: &Pat<'_>, shadowed: HirId, span: Span) {
179204
let (lint, msg) = match find_init(cx, pat.hir_id) {
180-
Some(expr) if is_self_shadow(cx, pat, expr, shadowed) => {
205+
Some((expr, _)) if is_self_shadow(cx, pat, expr, shadowed) => {
181206
let msg = format!(
182207
"`{}` is shadowed by itself in `{}`",
183208
snippet(cx, pat.span, "_"),
184209
snippet(cx, expr.span, "..")
185210
);
186211
(SHADOW_SAME, msg)
187212
},
188-
Some(expr) if is_local_used(cx, expr, shadowed) => {
213+
Some((expr, except)) if is_local_used_except(cx, expr, shadowed, except) => {
189214
let msg = format!("`{}` is shadowed", snippet(cx, pat.span, "_"));
190215
(SHADOW_REUSE, msg)
191216
},
@@ -232,15 +257,32 @@ fn is_self_shadow(cx: &LateContext<'_>, pat: &Pat<'_>, mut expr: &Expr<'_>, hir_
232257

233258
/// Finds the "init" expression for a pattern: `let <pat> = <init>;` (or `if let`) or
234259
/// `match <init> { .., <pat> => .., .. }`
235-
fn find_init<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId) -> Option<&'tcx Expr<'tcx>> {
236-
for (_, node) in cx.tcx.hir().parent_iter(hir_id) {
260+
///
261+
/// For closure arguments passed to a method call, returns the method call, and the `HirId` of the
262+
/// closure (which will later be skipped). This is for <https://github.com/rust-lang/rust-clippy/issues/10780>
263+
fn find_init<'tcx>(cx: &LateContext<'tcx>, hir_id: HirId) -> Option<(&'tcx Expr<'tcx>, Option<HirId>)> {
264+
for (hir_id, node) in cx.tcx.hir().parent_iter(hir_id) {
237265
let init = match node {
238-
Node::Arm(_) | Node::Pat(_) => continue,
266+
Node::Arm(_) | Node::Pat(_) | Node::Param(_) => continue,
239267
Node::Expr(expr) => match expr.kind {
240-
ExprKind::Match(e, _, _) | ExprKind::Let(&LetExpr { init: e, .. }) => Some(e),
268+
ExprKind::Match(e, _, _) | ExprKind::Let(&LetExpr { init: e, .. }) => Some((e, None)),
269+
// If we're a closure argument, then a parent call is also an associated item.
270+
ExprKind::Closure(_) => {
271+
if let Some((_, node)) = cx.tcx.hir().parent_iter(hir_id).next() {
272+
match node {
273+
Node::Expr(expr) => match expr.kind {
274+
ExprKind::MethodCall(_, _, _, _) | ExprKind::Call(_, _) => Some((expr, Some(hir_id))),
275+
_ => None,
276+
},
277+
_ => None,
278+
}
279+
} else {
280+
None
281+
}
282+
},
241283
_ => None,
242284
},
243-
Node::LetStmt(local) => local.init,
285+
Node::LetStmt(local) => local.init.map(|init| (init, None)),
244286
_ => None,
245287
};
246288
return init;

tests/ui/shadow.rs

+14
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,18 @@ fn ice_8748() {
119119
}];
120120
}
121121

122+
// https://github.com/rust-lang/rust-clippy/issues/10780
123+
fn shadow_closure() {
124+
// These are not shadow_unrelated; but they are correctly shadow_reuse
125+
let x = Some(1);
126+
#[allow(clippy::shadow_reuse)]
127+
let y = x.map(|x| x + 1);
128+
let z = x.map(|x| x + 1);
129+
let a: Vec<Option<u8>> = [100u8, 120, 140]
130+
.iter()
131+
.map(|i| i.checked_mul(2))
132+
.map(|i| i.map(|i| i - 10))
133+
.collect();
134+
}
135+
122136
fn main() {}

tests/ui/shadow.stderr

+25-1
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,29 @@ note: previous binding is here
280280
LL | let x = 1;
281281
| ^
282282

283-
error: aborting due to 23 previous errors
283+
error: `x` is shadowed
284+
--> tests/ui/shadow.rs:128:20
285+
|
286+
LL | let z = x.map(|x| x + 1);
287+
| ^
288+
|
289+
note: previous binding is here
290+
--> tests/ui/shadow.rs:125:9
291+
|
292+
LL | let x = Some(1);
293+
| ^
294+
295+
error: `i` is shadowed
296+
--> tests/ui/shadow.rs:132:25
297+
|
298+
LL | .map(|i| i.map(|i| i - 10))
299+
| ^
300+
|
301+
note: previous binding is here
302+
--> tests/ui/shadow.rs:132:15
303+
|
304+
LL | .map(|i| i.map(|i| i - 10))
305+
| ^
306+
307+
error: aborting due to 25 previous errors
284308

0 commit comments

Comments
 (0)