Skip to content

Commit 30577e2

Browse files
committed
Handle additional cases from #14396
1 parent 63c9fed commit 30577e2

8 files changed

+165
-99
lines changed

clippy_lints/src/methods/implicit_clone.rs

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::source::snippet_with_context;
33
use clippy_utils::ty::implements_trait;
4-
use clippy_utils::{is_diag_item_method, is_diag_trait_item, peel_middle_ty_refs};
4+
use clippy_utils::{get_parent_expr, is_diag_item_method, is_diag_trait_item, peel_middle_ty_refs};
55
use rustc_errors::Applicability;
66
use rustc_hir as hir;
77
use rustc_lint::LateContext;
88
use rustc_span::sym;
9+
use std::ops::ControlFlow;
910

1011
use super::IMPLICIT_CLONE;
1112

@@ -20,6 +21,7 @@ pub fn check(cx: &LateContext<'_>, method_name: &str, expr: &hir::Expr<'_>, recv
2021
&& return_type == input_type
2122
&& let Some(clone_trait) = cx.tcx.lang_items().clone_trait()
2223
&& implements_trait(cx, return_type, clone_trait, &[])
24+
&& is_called_from_map_like(cx, expr).is_none()
2325
{
2426
let mut app = Applicability::MachineApplicable;
2527
let recv_snip = snippet_with_context(cx, recv.span, expr.span.ctxt(), "..", &mut app).0;
@@ -56,3 +58,38 @@ pub fn is_clone_like(cx: &LateContext<'_>, method_name: &str, method_def_id: hir
5658
_ => false,
5759
}
5860
}
61+
62+
fn is_called_from_map_like(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<rustc_span::Span> {
63+
// Look for a closure as parent of `expr`, discarding simple blocks
64+
let parent_closure = cx
65+
.tcx
66+
.hir_parent_iter(expr.hir_id)
67+
.try_fold(expr.hir_id, |child_hir_id, (_, node)| match node {
68+
// Check that the child expression is the only expression in the block
69+
hir::Node::Block(block) if block.stmts.is_empty() && block.expr.map(|e| e.hir_id) == Some(child_hir_id) => {
70+
ControlFlow::Continue(block.hir_id)
71+
},
72+
hir::Node::Expr(expr) if matches!(expr.kind, hir::ExprKind::Block(..)) => {
73+
ControlFlow::Continue(expr.hir_id)
74+
},
75+
hir::Node::Expr(expr) if matches!(expr.kind, hir::ExprKind::Closure(_)) => ControlFlow::Break(Some(expr)),
76+
_ => ControlFlow::Break(None),
77+
})
78+
.break_value()?;
79+
is_parent_map_like(cx, parent_closure?)
80+
}
81+
82+
fn is_parent_map_like(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> Option<rustc_span::Span> {
83+
if let Some(parent_expr) = get_parent_expr(cx, expr)
84+
&& let hir::ExprKind::MethodCall(name, _, _, parent_span) = parent_expr.kind
85+
&& name.ident.name == sym::map
86+
&& let Some(caller_def_id) = cx.typeck_results().type_dependent_def_id(parent_expr.hir_id)
87+
&& (is_diag_item_method(cx, caller_def_id, sym::Result)
88+
|| is_diag_item_method(cx, caller_def_id, sym::Option)
89+
|| is_diag_trait_item(cx, caller_def_id, sym::Iterator))
90+
{
91+
Some(parent_span)
92+
} else {
93+
None
94+
}
95+
}

clippy_lints/src/methods/map_clone.rs

+64-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::implicit_clone::is_clone_like;
12
use clippy_utils::diagnostics::span_lint_and_sugg;
23
use clippy_utils::msrvs::{self, Msrv};
34
use clippy_utils::source::snippet_with_applicability;
@@ -8,8 +9,8 @@ use rustc_hir::def_id::DefId;
89
use rustc_hir::{self as hir, LangItem};
910
use rustc_lint::LateContext;
1011
use rustc_middle::mir::Mutability;
11-
use rustc_middle::ty;
1212
use rustc_middle::ty::adjustment::Adjust;
13+
use rustc_middle::ty::{self, Ty};
1314
use rustc_span::symbol::Ident;
1415
use rustc_span::{Span, sym};
1516

@@ -67,22 +68,29 @@ pub(super) fn check(cx: &LateContext<'_>, e: &hir::Expr<'_>, recv: &hir::Expr<'_
6768
}
6869
},
6970
hir::ExprKind::MethodCall(method, obj, [], _) => {
70-
if ident_eq(name, obj) && method.ident.name == sym::clone
71+
if ident_eq(name, obj)
7172
&& let Some(fn_id) = cx.typeck_results().type_dependent_def_id(closure_expr.hir_id)
72-
&& let Some(trait_id) = cx.tcx.trait_of_item(fn_id)
73-
&& cx.tcx.lang_items().clone_trait() == Some(trait_id)
73+
&& (is_clone(cx, method.ident.name, fn_id)
74+
|| is_clone_like(cx, method.ident.as_str(), fn_id))
7475
// no autoderefs
7576
&& !cx.typeck_results().expr_adjustments(obj).iter()
7677
.any(|a| matches!(a.kind, Adjust::Deref(Some(..))))
78+
&& let obj_ty = cx.typeck_results().expr_ty_adjusted(obj)
79+
&& let ty::Ref(_, ty, Mutability::Not) = obj_ty.kind()
80+
// Verify that the method call's output type is the same as its input type. This is to
81+
// avoid cases like `to_string` being called on a `&str`, or `to_vec` being called on a
82+
// slice.
83+
&& *ty == cx.typeck_results().expr_ty_adjusted(closure_expr)
7784
{
78-
let obj_ty = cx.typeck_results().expr_ty(obj);
79-
if let ty::Ref(_, ty, mutability) = obj_ty.kind() {
80-
if matches!(mutability, Mutability::Not) {
81-
let copy = is_copy(cx, *ty);
82-
lint_explicit_closure(cx, e.span, recv.span, copy, msrv);
83-
}
85+
let obj_ty_unadjusted = cx.typeck_results().expr_ty(obj);
86+
if obj_ty == obj_ty_unadjusted {
87+
let copy = is_copy(cx, *ty);
88+
lint_explicit_closure(cx, e.span, recv.span, copy, msrv);
8489
} else {
85-
lint_needless_cloning(cx, e.span, recv.span);
90+
// To avoid issue #6299, ensure `obj` is not a mutable reference.
91+
if !matches!(obj_ty_unadjusted.kind(), ty::Ref(_, _, Mutability::Mut)) {
92+
lint_needless_cloning(cx, e.span, recv.span);
93+
}
8694
}
8795
}
8896
},
@@ -105,29 +113,66 @@ pub(super) fn check(cx: &LateContext<'_>, e: &hir::Expr<'_>, recv: &hir::Expr<'_
105113
}
106114
}
107115

116+
fn is_clone(cx: &LateContext<'_>, method: rustc_span::Symbol, fn_id: DefId) -> bool {
117+
if method == sym::clone
118+
&& let Some(trait_id) = cx.tcx.trait_of_item(fn_id)
119+
&& cx.tcx.lang_items().clone_trait() == Some(trait_id)
120+
{
121+
true
122+
} else {
123+
false
124+
}
125+
}
126+
108127
fn handle_path(
109128
cx: &LateContext<'_>,
110129
arg: &hir::Expr<'_>,
111130
qpath: &hir::QPath<'_>,
112131
e: &hir::Expr<'_>,
113132
recv: &hir::Expr<'_>,
114133
) {
134+
let method = || match qpath {
135+
hir::QPath::TypeRelative(_, method) => Some(method),
136+
_ => None,
137+
};
115138
if let Some(path_def_id) = cx.qpath_res(qpath, arg.hir_id).opt_def_id()
116-
&& cx.tcx.lang_items().get(LangItem::CloneFn) == Some(path_def_id)
139+
&& (cx.tcx.lang_items().get(LangItem::CloneFn) == Some(path_def_id)
140+
|| method().is_some_and(|method| is_clone_like(cx, method.ident.name.as_str(), path_def_id)))
117141
// The `copied` and `cloned` methods are only available on `&T` and `&mut T` in `Option`
118142
// and `Result`.
119-
&& let ty::Adt(_, args) = cx.typeck_results().expr_ty(recv).kind()
120-
&& let args = args.as_slice()
121-
&& let Some(ty) = args.iter().find_map(|generic_arg| generic_arg.as_type())
122-
&& let ty::Ref(_, ty, Mutability::Not) = ty.kind()
123-
&& let ty::FnDef(_, lst) = cx.typeck_results().expr_ty(arg).kind()
124-
&& lst.iter().all(|l| l.as_type() == Some(*ty))
125-
&& !should_call_clone_as_function(cx, *ty)
143+
// Previously, `handle_path` would make this determination by checking the type of `recv`.
144+
// Specifically, `handle_path` would check whether `recv`'s type was instantiated with a
145+
// reference. However, that approach can produce false negatives. Consider
146+
// `std::slice::Iter`, for example. Even if it is instantiated with a non-reference type,
147+
// its `map` method will expect a function that operates on references.
148+
&& let Some(ty) = clone_like_referent(cx, arg)
149+
&& !should_call_clone_as_function(cx, ty)
126150
{
127151
lint_path(cx, e.span, recv.span, is_copy(cx, ty.peel_refs()));
128152
}
129153
}
130154

155+
/// Determine whether `expr` is function whose input type is `&T` and whose output type is `T`. If
156+
/// so, return `T`.
157+
fn clone_like_referent<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>) -> Option<Ty<'tcx>> {
158+
let ty = cx.typeck_results().expr_ty_adjusted(expr);
159+
if let ty::FnDef(def_id, generic_args) = ty.kind()
160+
&& let tys = cx
161+
.tcx
162+
.fn_sig(def_id)
163+
.instantiate(cx.tcx, generic_args)
164+
.skip_binder()
165+
.inputs_and_output
166+
&& let [input_ty, output_ty] = tys.as_slice()
167+
&& let ty::Ref(_, referent_ty, Mutability::Not) = input_ty.kind()
168+
&& referent_ty == output_ty
169+
{
170+
Some(*referent_ty)
171+
} else {
172+
None
173+
}
174+
}
175+
131176
fn ident_eq(name: Ident, path: &hir::Expr<'_>) -> bool {
132177
if let hir::ExprKind::Path(hir::QPath::Resolved(None, path)) = path.kind {
133178
path.segments.len() == 1 && path.segments[0].ident == name

tests/ui/map_clone.fixed

+16
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,20 @@ fn main() {
158158
let y = Some(&x);
159159
let _z = y.map(RcWeak::clone);
160160
}
161+
162+
let variable1 = String::new();
163+
let v = &variable1;
164+
let variable2 = Some(v);
165+
let _ = variable2.cloned();
166+
//~^ map_clone
167+
let _ = variable2.cloned();
168+
//~^ map_clone
169+
#[rustfmt::skip]
170+
let _ = variable2.cloned();
171+
//~^ map_clone
172+
173+
let _ = vec![String::new()].iter().cloned().collect::<Vec<_>>();
174+
//~^ map_clone
175+
let _ = vec![String::new()].iter().cloned().collect::<Vec<_>>();
176+
//~^ map_clone
161177
}

tests/ui/map_clone.rs

+16
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,20 @@ fn main() {
158158
let y = Some(&x);
159159
let _z = y.map(RcWeak::clone);
160160
}
161+
162+
let variable1 = String::new();
163+
let v = &variable1;
164+
let variable2 = Some(v);
165+
let _ = variable2.map(String::to_string);
166+
//~^ map_clone
167+
let _ = variable2.map(|x| x.to_string());
168+
//~^ map_clone
169+
#[rustfmt::skip]
170+
let _ = variable2.map(|x| { x.to_string() });
171+
//~^ map_clone
172+
173+
let _ = vec![String::new()].iter().map(String::to_string).collect::<Vec<_>>();
174+
//~^ map_clone
175+
let _ = vec![String::new()].iter().map(|x| x.to_string()).collect::<Vec<_>>();
176+
//~^ map_clone
161177
}

tests/ui/map_clone.stderr

+31-1
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,35 @@ error: you are explicitly cloning with `.map()`
9191
LL | let y = x.map(|x| Clone::clone(x));
9292
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `copied` method: `x.copied()`
9393

94-
error: aborting due to 15 previous errors
94+
error: you are explicitly cloning with `.map()`
95+
--> tests/ui/map_clone.rs:165:13
96+
|
97+
LL | let _ = variable2.map(String::to_string);
98+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `cloned` method: `variable2.cloned()`
99+
100+
error: you are using an explicit closure for cloning elements
101+
--> tests/ui/map_clone.rs:167:13
102+
|
103+
LL | let _ = variable2.map(|x| x.to_string());
104+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `cloned` method: `variable2.cloned()`
105+
106+
error: you are using an explicit closure for cloning elements
107+
--> tests/ui/map_clone.rs:170:13
108+
|
109+
LL | let _ = variable2.map(|x| { x.to_string() });
110+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `cloned` method: `variable2.cloned()`
111+
112+
error: you are explicitly cloning with `.map()`
113+
--> tests/ui/map_clone.rs:173:13
114+
|
115+
LL | let _ = vec![String::new()].iter().map(String::to_string).collect::<Vec<_>>();
116+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `cloned` method: `vec![String::new()].iter().cloned()`
117+
118+
error: you are using an explicit closure for cloning elements
119+
--> tests/ui/map_clone.rs:175:13
120+
|
121+
LL | let _ = vec![String::new()].iter().map(|x| x.to_string()).collect::<Vec<_>>();
122+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider calling the dedicated `cloned` method: `vec![String::new()].iter().cloned()`
123+
124+
error: aborting due to 20 previous errors
95125

tests/ui/string_to_string_in_map.fixed

-20
This file was deleted.

tests/ui/string_to_string_in_map.rs

-20
This file was deleted.

tests/ui/string_to_string_in_map.stderr

-38
This file was deleted.

0 commit comments

Comments
 (0)