Skip to content

Lint for using hand-writing a fold with the same behaviour as any #2350

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

Merged
merged 15 commits into from
Jan 19, 2018
Merged
Show file tree
Hide file tree
Changes from 8 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
12 changes: 2 additions & 10 deletions clippy_lints/src/map_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use rustc::lint::*;
use rustc::hir::*;
use rustc::ty;
use syntax::ast;
use utils::{is_adjusted, iter_input_pats, match_qpath, match_trait_method, match_type, paths, remove_blocks, snippet,
span_help_and_lint, walk_ptrs_ty, walk_ptrs_ty_depth};
use utils::{get_arg_name, is_adjusted, iter_input_pats, match_qpath, match_trait_method, match_type,
paths, remove_blocks, snippet, span_help_and_lint, walk_ptrs_ty, walk_ptrs_ty_depth};

/// **What it does:** Checks for mapping `clone()` over an iterator.
///
Expand Down Expand Up @@ -121,14 +121,6 @@ fn get_type_name(cx: &LateContext, expr: &Expr, arg: &Expr) -> Option<&'static s
}
}

fn get_arg_name(pat: &Pat) -> Option<ast::Name> {
match pat.node {
PatKind::Binding(_, _, name, None) => Some(name.node),
PatKind::Ref(ref subpat, _) => get_arg_name(subpat),
_ => None,
}
}

fn only_derefs(cx: &LateContext, expr: &Expr, id: ast::Name) -> bool {
match expr.node {
ExprUnary(UnDeref, ref subexpr) if !is_adjusted(cx, subexpr) => only_derefs(cx, subexpr, id),
Expand Down
80 changes: 76 additions & 4 deletions clippy_lints/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use std::borrow::Cow;
use std::fmt;
use std::iter;
use syntax::ast;
use syntax::codemap::Span;
use utils::{get_trait_def_id, implements_trait, in_external_macro, in_macro, is_copy, is_self, is_self_ty,
use syntax::codemap::{Span, BytePos};
use utils::{get_arg_name, get_trait_def_id, implements_trait, in_external_macro, in_macro, is_copy, is_self, is_self_ty,
iter_input_pats, last_path_segment, match_def_path, match_path, match_qpath, match_trait_method,
match_type, method_chain_args, return_ty, same_tys, single_segment_path, snippet, span_lint,
match_type, method_chain_args, return_ty, remove_blocks, same_tys, single_segment_path, snippet, span_lint,
span_lint_and_sugg, span_lint_and_then, span_note_and_lint, walk_ptrs_ty, walk_ptrs_ty_depth};
use utils::paths;
use utils::sugg;
Expand Down Expand Up @@ -623,6 +623,23 @@ declare_lint! {
"using `as_ref` where the types before and after the call are the same"
}


/// **What it does:** Checks for using `fold` to implement `any`.
///
/// **Why is this bad?** Readability.
///
/// **Known problems:** Changes semantics - the suggested replacement is short-circuiting.
///
/// **Example:**
/// ```rust
/// let _ = (0..3).fold(false, |acc, x| acc || x > 2);
/// ```
declare_lint! {
pub FOLD_ANY,
Warn,
"using `fold` to emulate the behaviour of `any`"
}

impl LintPass for Pass {
fn get_lints(&self) -> LintArray {
lint_array!(
Expand Down Expand Up @@ -653,7 +670,8 @@ impl LintPass for Pass {
GET_UNWRAP,
STRING_EXTEND_CHARS,
ITER_CLONED_COLLECT,
USELESS_ASREF
USELESS_ASREF,
FOLD_ANY
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Does this macro support trailing commas? If so, please add one and the next diff is only one line :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It appears not:

error: unexpected end of macro invocation
   --> clippy_lints/src/methods.rs:674:21
    |
674 |             FOLD_ANY,
    |                     ^

Copy link
Member

Choose a reason for hiding this comment

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

Ugh, that's too bad. Someone should make a pull request to change this :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've created an issue #2352. I'll do this next weekend if it's still open.

Copy link
Member

@killercup killercup Jan 14, 2018

Choose a reason for hiding this comment

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

Oh, this is a macro defined in the compiler, so that issue is probably more useful over at https://github.com/rust-lang/rust :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

)
}
}
Expand Down Expand Up @@ -717,6 +735,8 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for Pass {
lint_asref(cx, expr, "as_ref", arglists[0]);
} else if let Some(arglists) = method_chain_args(expr, &["as_mut"]) {
lint_asref(cx, expr, "as_mut", arglists[0]);
} else if let Some(arglists) = method_chain_args(expr, &["fold"]) {
lint_fold_any(cx, expr, arglists[0]);
}

lint_or_fun_call(cx, expr, &method_call.name.as_str(), args);
Expand Down Expand Up @@ -1105,6 +1125,58 @@ fn lint_iter_cloned_collect(cx: &LateContext, expr: &hir::Expr, iter_args: &[hir
}
}

fn lint_fold_any(cx: &LateContext, expr: &hir::Expr, fold_args: &[hir::Expr]) {
// Check that this is a call to Iterator::fold rather than just some function called fold
if !match_trait_method(cx, expr, &paths::ITERATOR) {
return;
}

assert!(fold_args.len() == 3,
"Expected fold_args to have three entries - the receiver, the initial value and the closure");
Copy link
Member

Choose a reason for hiding this comment

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

Should this actually panic? I imagine a debug! and return; suffices

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, it's a logic bug, so I would have thought it should panic. I don't feel strongly though - I can change if it you'd prefer.


if_chain! {
// Check if the initial value for the fold is the literal `false`
if let hir::ExprLit(ref lit) = fold_args[1].node;
if lit.node == ast::LitKind::Bool(false);

// Extract the body of the closure passed to fold
if let hir::ExprClosure(_, _, body_id, _, _) = fold_args[2].node;
let closure_body = cx.tcx.hir.body(body_id);
let closure_expr = remove_blocks(&closure_body.value);

// Extract the names of the two arguments to the closure
if let Some(first_arg_ident) = get_arg_name(&closure_body.arguments[0].pat);
if let Some(second_arg_ident) = get_arg_name(&closure_body.arguments[1].pat);

// Check if the closure body is of the form `acc || some_expr(x)`
if let hir::ExprBinary(ref bin_op, ref left_expr, ref right_expr) = closure_expr.node;
if bin_op.node == hir::BinOp_::BiOr;
if let hir::ExprPath(hir::QPath::Resolved(None, ref path)) = left_expr.node;
if path.segments.len() == 1 && &path.segments[0].name == &first_arg_ident;

then {
let right_source = snippet(cx, right_expr.span, "EXPR");

// Span containing `.fold(...)`
let fold_span = fold_args[0].span.next_point().with_hi(fold_args[2].span.hi() + BytePos(1));

span_lint_and_sugg(
cx,
FOLD_ANY,
fold_span,
// TODO: don't suggest .any(|x| f(x)) if we can suggest .any(f)
Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine, we do have another lint for that ;)

If you really want to, you can extract the code from there into a separate function so you can call it from here and there and not duplicate any logic.

"this `.fold` can more succintly be expressed as `.any`",
"try",
format!(
".any(|{s}| {r})",
s = second_arg_ident,
r = right_source
)
);
}
}
}

fn lint_iter_nth(cx: &LateContext, expr: &hir::Expr, iter_args: &[hir::Expr], is_mut: bool) {
let mut_str = if is_mut { "_mut" } else { "" };
let caller_type = if derefs_to_slice(cx, &iter_args[0], cx.tables.expr_ty(&iter_args[0])).is_some() {
Expand Down
22 changes: 22 additions & 0 deletions clippy_lints/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,20 @@ pub fn span_lint_and_then<'a, 'tcx: 'a, T: LintContext<'tcx>, F>(
db.docs_link(lint);
}

/// Add a span lint with a suggestion on how to fix it.
///
/// These suggestions can be parsed by rustfix to allow it to automatically fix your code.
/// In the example below, `help` is `"try"` and `sugg` is the suggested replacement `".any(|x| x > 2)"`.
///
/// <pre>
/// error: This `.fold` can be more succinctly expressed as `.any`
/// --> $DIR/methods.rs:390:13
/// |
/// 390 | let _ = (0..3).fold(false, |acc, x| acc || x > 2);
/// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `.any(|x| x > 2)`
/// |
/// = note: `-D fold-any` implied by `-D warnings`
/// </pre>
Copy link
Member

Choose a reason for hiding this comment

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

replace <pre> with triple back ticks or 4-space indent

pub fn span_lint_and_sugg<'a, 'tcx: 'a, T: LintContext<'tcx>>(
cx: &'a T,
lint: &'static Lint,
Expand Down Expand Up @@ -1034,3 +1048,11 @@ pub fn type_size<'a, 'tcx>(cx: &LateContext<'a, 'tcx>, ty: Ty<'tcx>) -> Option<u
pub fn is_allowed(cx: &LateContext, lint: &'static Lint, id: NodeId) -> bool {
cx.tcx.lint_level_at_node(lint, id).0 == Level::Allow
}

pub fn get_arg_name(pat: &Pat) -> Option<ast::Name> {
match pat.node {
PatKind::Binding(_, _, name, None) => Some(name.node),
PatKind::Ref(ref subpat, _) => get_arg_name(subpat),
_ => None,
}
}
20 changes: 20 additions & 0 deletions tests/ui/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,26 @@ fn iter_skip_next() {
let _ = foo.filter().skip(42).next();
}

/// Should trigger the `FOLD_ANY` lint
fn fold_any() {
let _ = (0..3).fold(false, |acc, x| acc || x > 2);
}

/// Should not trigger the `FOLD_ANY` lint as the initial value is not the literal `false`
fn fold_any_ignores_initial_value_of_true() {
let _ = (0..3).fold(true, |acc, x| acc || x > 2);
}
Copy link
Member

Choose a reason for hiding this comment

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

You might also want to add another test that doesn't trigger the lint because it's not using a boolean, e.g. .fold(0, |acc, x| acc += if x > 2 { 1 } else { 0 })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do.


/// Should not trigger the `FOLD_ANY` lint as the accumulator is not integer valued
fn fold_any_ignores_non_boolean_accumalator() {
let _ = (0..3).fold(0, |acc, x| acc + if x > 2 { 1 } else { 0 });
}

/// Should trigger the `FOLD_ANY` lint, with the error span including exactly `.fold(...)`
fn fold_any_span_for_multi_element_chain() {
let _ = (0..3).map(|x| 2 * x).fold(false, |acc, x| acc || x > 2);
}

#[allow(similar_names)]
fn main() {
let opt = Some(0);
Expand Down
18 changes: 16 additions & 2 deletions tests/ui/methods.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -493,10 +493,24 @@ error: called `skip(x).next()` on an iterator. This is more succinctly expressed
382 | let _ = &some_vec[..].iter().skip(3).next();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: this `.fold` can more succintly be expressed as `.any`
--> $DIR/methods.rs:390:19
|
390 | let _ = (0..3).fold(false, |acc, x| acc || x > 2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `.any(|x| x > 2)`
|
= note: `-D fold-any` implied by `-D warnings`

error: this `.fold` can more succintly be expressed as `.any`
--> $DIR/methods.rs:405:34
|
405 | let _ = (0..3).map(|x| 2 * x).fold(false, |acc, x| acc || x > 2);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `.any(|x| x > 2)`

error: used unwrap() on an Option value. If you don't want to handle the None case gracefully, consider using expect() to provide a better panic message
--> $DIR/methods.rs:391:13
--> $DIR/methods.rs:411:13
|
391 | let _ = opt.unwrap();
411 | let _ = opt.unwrap();
| ^^^^^^^^^^^^
|
= note: `-D option-unwrap-used` implied by `-D warnings`
Expand Down