Skip to content
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

Suggest for loop instead of while-let when looping over iterators #396

Merged
merged 6 commits into from
Oct 27, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A collection of lints to catch common mistakes and improve your Rust code.
[Jump to usage instructions](#usage)

##Lints
There are 68 lints included in this crate:
There are 69 lints included in this crate:

name | default | meaning
-------------------------------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -74,6 +74,7 @@ name
[unstable_as_slice](https://github.com/Manishearth/rust-clippy/wiki#unstable_as_slice) | warn | as_slice is not stable and can be replaced by & v[..]see https://github.com/rust-lang/rust/issues/27729
[unused_collect](https://github.com/Manishearth/rust-clippy/wiki#unused_collect) | warn | `collect()`ing an iterator without using the result; this is usually better written as a for loop
[while_let_loop](https://github.com/Manishearth/rust-clippy/wiki#while_let_loop) | warn | `loop { if let { ... } else break }` can be written as a `while let` loop
[while_let_on_iterator](https://github.com/Manishearth/rust-clippy/wiki#while_let_on_iterator) | warn | using a while-let loop instead of a for loop on an iterator
[wrong_pub_self_convention](https://github.com/Manishearth/rust-clippy/wiki#wrong_pub_self_convention) | allow | defining a public method named with an established prefix (like "into_") that takes `self` with the wrong convention
[wrong_self_convention](https://github.com/Manishearth/rust-clippy/wiki#wrong_self_convention) | warn | defining a method named with an established prefix (like "into_") that takes `self` with the wrong convention
[zero_divided_by_zero](https://github.com/Manishearth/rust-clippy/wiki#zero_divided_by_zero) | warn | usage of `0.0 / 0.0` to obtain NaN instead of std::f32::NaN or std::f64::NaN
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub fn plugin_registrar(reg: &mut Registry) {
loops::REVERSE_RANGE_LOOP,
loops::UNUSED_COLLECT,
loops::WHILE_LET_LOOP,
loops::WHILE_LET_ON_ITERATOR,
matches::MATCH_BOOL,
matches::MATCH_REF_PATS,
matches::SINGLE_MATCH,
Expand Down
70 changes: 68 additions & 2 deletions src/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use std::collections::{HashSet,HashMap};
use syntax::ast::Lit_::*;

use utils::{snippet, span_lint, get_parent_expr, match_trait_method, match_type,
in_external_macro, expr_block, span_help_and_lint, is_integer_literal};
in_external_macro, expr_block, span_help_and_lint, is_integer_literal,
get_enclosing_block};
use utils::{VEC_PATH, LL_PATH};

declare_lint!{ pub NEEDLESS_RANGE_LOOP, Warn,
Expand All @@ -38,14 +39,17 @@ declare_lint!{ pub EXPLICIT_COUNTER_LOOP, Warn,

declare_lint!{ pub EMPTY_LOOP, Warn, "empty `loop {}` detected" }

declare_lint!{ pub WHILE_LET_ON_ITERATOR, Warn, "using a while-let loop instead of a for loop on an iterator" }

#[derive(Copy, Clone)]
pub struct LoopsPass;

impl LintPass for LoopsPass {
fn get_lints(&self) -> LintArray {
lint_array!(NEEDLESS_RANGE_LOOP, EXPLICIT_ITER_LOOP, ITER_NEXT_LOOP,
WHILE_LET_LOOP, UNUSED_COLLECT, REVERSE_RANGE_LOOP,
EXPLICIT_COUNTER_LOOP, EMPTY_LOOP)
EXPLICIT_COUNTER_LOOP, EMPTY_LOOP,
WHILE_LET_ON_ITERATOR)
}
}

Expand Down Expand Up @@ -228,6 +232,28 @@ impl LateLintPass for LoopsPass {
}
}
}
if let ExprMatch(ref match_expr, ref arms, MatchSource::WhileLetDesugar) = expr.node {
let pat = &arms[0].pats[0].node;
if let (&PatEnum(ref path, Some(ref pat_args)),
&ExprMethodCall(method_name, _, ref method_args)) =
(pat, &match_expr.node) {
let iter_expr = &method_args[0];
if let Some(lhs_constructor) = path.segments.last() {
if method_name.node.as_str() == "next" &&
match_trait_method(cx, match_expr, &["core", "iter", "Iterator"]) &&
lhs_constructor.identifier.name.as_str() == "Some" &&
!is_iterator_used_after_while_let(cx, iter_expr) {
let iterator = snippet(cx, method_args[0].span, "_");
let loop_var = snippet(cx, pat_args[0].span, "_");
span_help_and_lint(cx, WHILE_LET_ON_ITERATOR, expr.span,
"this loop could be written as a `for` loop",
&format!("try\nfor {} in {} {{...}}",
loop_var,
iterator));
}
}
}
}
}

fn check_stmt(&mut self, cx: &LateContext, stmt: &Stmt) {
Expand Down Expand Up @@ -300,6 +326,46 @@ impl<'v, 't> Visitor<'v> for VarVisitor<'v, 't> {
}
}

fn is_iterator_used_after_while_let(cx: &LateContext, iter_expr: &Expr) -> bool {
let def_id = match var_def_id(cx, iter_expr) {
Some(id) => id,
None => return false
};
let mut visitor = VarUsedAfterLoopVisitor {
cx: cx,
def_id: def_id,
iter_expr_id: iter_expr.id,
past_while_let: false,
var_used_after_while_let: false
};
if let Some(enclosing_block) = get_enclosing_block(cx, def_id) {
walk_block(&mut visitor, enclosing_block);
}
visitor.var_used_after_while_let
}

struct VarUsedAfterLoopVisitor<'v, 't: 'v> {
cx: &'v LateContext<'v, 't>,
def_id: NodeId,
iter_expr_id: NodeId,
past_while_let: bool,
var_used_after_while_let: bool
}

impl <'v, 't> Visitor<'v> for VarUsedAfterLoopVisitor<'v, 't> {
fn visit_expr(&mut self, expr: &'v Expr) {
if self.past_while_let {
if Some(self.def_id) == var_def_id(self.cx, expr) {
self.var_used_after_while_let = true;
}
} else if self.iter_expr_id == expr.id {
self.past_while_let = true;
}
walk_expr(self, expr);
}
}


/// Return true if the type of expr is one that provides IntoIterator impls
/// for &T and &mut T, such as Vec.
fn is_ref_iterable_type(cx: &LateContext, e: &Expr) -> bool {
Expand Down
14 changes: 14 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,20 @@ pub fn get_parent_expr<'c>(cx: &'c LateContext, e: &Expr) -> Option<&'c Expr> {
if let NodeExpr(parent) = node { Some(parent) } else { None } )
}

#[allow(needless_lifetimes)] // workaround for https://github.com/Manishearth/rust-clippy/issues/417
pub fn get_enclosing_block<'c>(cx: &'c LateContext, node: NodeId) -> Option<&'c Block> {
let map = &cx.tcx.map;
let enclosing_node = map.get_enclosing_scope(node)
.and_then(|enclosing_id| map.find(enclosing_id));
if let Some(node) = enclosing_node {
match node {
NodeBlock(ref block) => Some(block),
NodeItem(&Item{ node: ItemFn(_, _, _, _, _, ref block), .. }) => Some(block),
_ => None
}
} else { None }
}

#[cfg(not(feature="structured_logging"))]
pub fn span_lint<T: LintContext>(cx: &T, lint: &'static Lint, sp: Span, msg: &str) {
cx.span_lint(lint, sp, msg);
Expand Down
46 changes: 45 additions & 1 deletion tests/compile-fail/while_loop.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![feature(plugin)]
#![plugin(clippy)]

#![deny(while_let_loop, empty_loop)]
#![deny(while_let_loop, empty_loop, while_let_on_iterator)]
#![allow(dead_code, unused)]

fn main() {
Expand Down Expand Up @@ -53,6 +53,50 @@ fn main() {
while let Some(x) = y { // no error, obviously
println!("{}", x);
}

let mut iter = 1..20;
while let Option::Some(x) = iter.next() { //~ERROR this loop could be written as a `for` loop
println!("{}", x);
}

let mut iter = 1..20;
while let Some(x) = iter.next() { //~ERROR this loop could be written as a `for` loop
println!("{}", x);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's a contrived example, but what if the loop breaks and the iter is reused? I think for loops usually consume the iterator, am I right?

We should at least have a test case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iirc they borrow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, for loops consume the iterator (probably because IntoIterator::into_iter takes self by value). I actually considered that, but couldn't come up with convincing use-cases where you would want to use the iterator after the loop. On the other hand, it shouldn't be very hard to fix.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, you're right.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whew! You had me worried I was mis-remembering how iterators worked – and I already had three cups of coffee, so my brain should sort of work currently... 😄

Anyway, yeah, it shouldn't be too hard to detect and omit the lint in those situations.


let mut iter = 1..20;
while let Some(_) = iter.next() {} //~ERROR this loop could be written as a `for` loop

let mut iter = 1..20;
while let None = iter.next() {} // this is fine (if nonsensical)

let mut iter = 1..20;
if let Some(x) = iter.next() { // also fine
println!("{}", x)
}

// the following shouldn't warn because it can't be written with a for loop
let mut iter = 1u32..20;
while let Some(x) = iter.next() {
println!("next: {:?}", iter.next())
}

// neither can this
let mut iter = 1u32..20;
while let Some(x) = iter.next() {
println!("next: {:?}", iter.next());
}

// or this
let mut iter = 1u32..20;
while let Some(x) = iter.next() {break;}
println!("Remaining iter {:?}", iter);

// or this
let mut iter = 1u32..20;
while let Some(x) = iter.next() {
iter = 1..20;
}
}

// regression test (#360)
Expand Down