-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement a lint for .clone().into_iter()
#13595
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
use clippy_utils::diagnostics::span_lint_and_sugg; | ||
use clippy_utils::is_path_mutable; | ||
use clippy_utils::source::snippet_with_applicability; | ||
use clippy_utils::ty::{deref_chain, get_inherent_method, implements_trait, make_normalized_projection}; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::Expr; | ||
use rustc_lint::LateContext; | ||
use rustc_middle::ty::{self, Ty}; | ||
use rustc_span::sym; | ||
|
||
use super::{UNNECESSARY_COLLECTION_CLONE, method_call}; | ||
|
||
// FIXME: This does not check if the iter method is actually compatible with the replacement, but | ||
// you have to be actively evil to have an `IntoIterator` impl that returns one type and an `iter` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like this comment xD I wonder if there is actually a reasonable use case for this, but let's not open that can of worms ^^ |
||
// method that returns something other than references of that type.... and it is a massive | ||
// complicated hassle to check this | ||
fn has_iter_method<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { | ||
deref_chain(cx, ty).any(|ty| match ty.kind() { | ||
ty::Adt(adt_def, _) => get_inherent_method(cx, adt_def.did(), sym::iter).is_some(), | ||
ty::Slice(_) => true, | ||
_ => false, | ||
}) | ||
} | ||
|
||
/// Check for `x.clone().into_iter()` to suggest `x.iter().cloned()`. | ||
// ^^^^^^^^^ is recv | ||
// ^^^^^^^^^^^^^^^^^^^^^ is expr | ||
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>) { | ||
let typeck_results = cx.typeck_results(); | ||
let diagnostic_items = cx.tcx.all_diagnostic_items(()); | ||
|
||
// If the call before `into_iter` is `.clone()` | ||
if let Some(("clone", collection_expr, [], _, _)) = method_call(recv) | ||
// and the binding being cloned is not mutable | ||
&& let Some(false) = is_path_mutable(cx, collection_expr) | ||
// and the result of `into_iter` is an Iterator | ||
&& let Some(&iterator_def_id) = diagnostic_items.name_to_id.get(&sym::Iterator) | ||
&& let expr_ty = typeck_results.expr_ty(expr) | ||
&& implements_trait(cx, expr_ty, iterator_def_id, &[]) | ||
// with an item that implements clone | ||
&& let Some(&clone_def_id) = diagnostic_items.name_to_id.get(&sym::Clone) | ||
&& let Some(item_ty) = make_normalized_projection(cx.tcx, cx.param_env, iterator_def_id, sym::Item, [expr_ty]) | ||
&& implements_trait(cx, item_ty, clone_def_id, &[]) | ||
// and the type has an `iter` method | ||
&& has_iter_method(cx, typeck_results.expr_ty(collection_expr)) | ||
{ | ||
let mut applicability = Applicability::MachineApplicable; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is this lint trigger from Lintcheck:
These cases might not be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that's a shame, I hate relegating useful lints to pedantic where they won't be hit by the beginners this lint is targeting. Would it be good if I swap the suggestion to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The applicability is only given to tools like rustfix and not directly visible to the user. So changing the applicability, shouldn't have an effect on the lint category. The category usually depends on the FP rate and the trigger rate when we introduce the lint. If a lint is reasonable, but has too many lint triggers, it will sadly still often land in |
||
let collection_expr_snippet = snippet_with_applicability(cx, collection_expr.span, "...", &mut applicability); | ||
span_lint_and_sugg( | ||
cx, | ||
UNNECESSARY_COLLECTION_CLONE, | ||
expr.span, | ||
"using clone on collection to own iterated items", | ||
"replace with", | ||
format!("{collection_expr_snippet}.iter().cloned()"), | ||
applicability, | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this suggestion is always better. Have you benchmarked that individual clones are actually faster, or at least equivalent?
With a linked list (I know everyone's favorite structure) it might be legit better to clone it, thereby allocating all nodes close together and loading everything into cache.
This is not an argument against the lint, I still think it's a good idea, just a question about the category
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't benchmarked this, and I may be falling into the "it's somewhat obvious" trap. It's possible we could maintain a list of collections it would be quicker to do this, or even have a
#[clippy::iter_cloned_instead_of_clone)]
to mark collections that would benefit from this lint. But it will be quite the low FP rate for collections that are faster to clone than iterate and clone.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this highly depends on the iterator usage. For a vector, it makes a difference of allocating a big chunk of memory once against several small allocations. For vectors it might actually be the other way around (meaning
.clone().into_iter()
being faster), but this is just guesstimating, since I haven't benchmarked this.One thing that's safe to say, is that cloning each item individually will reduce the memory footprint, but only if the items are dropped and not stored somewhere afterward.
Could you try to benchmark this?
I've just checked the issue, and that one was suggestion to lint on
cloned().into_iter().filter()
. Linting on that should be safe again