-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from 12 commits
f6e56d2
1feb9fd
528be23
7e833ea
360f235
70a5535
ad16493
a64d19c
1cac693
29a2dd4
9806b31
b73efad
a324a2b
7030259
71abd81
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 |
---|---|---|
|
@@ -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; | ||
|
@@ -623,6 +623,29 @@ declare_lint! { | |
"using `as_ref` where the types before and after the call are the same" | ||
} | ||
|
||
|
||
/// **What it does:** Checks for using `fold` when a more succint alternative exists. | ||
/// Specifically, this checks for `fold`s which could be replaced by `any`, `all`, | ||
/// `sum` or `product`. | ||
/// | ||
/// **Why is this bad?** Readability. | ||
/// | ||
/// **Known problems:** None. | ||
/// | ||
/// **Example:** | ||
/// ```rust | ||
/// let _ = (0..3).fold(false, |acc, x| acc || x > 2); | ||
/// ``` | ||
/// This could be written as: | ||
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'm pretty sure it's good style to put an empty line above and below code blocks in markdown. 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 existing lints in this file don't, for whatever that's worth |
||
/// ```rust | ||
/// let _ = (0..3).any(|x| x > 2); | ||
/// ``` | ||
declare_lint! { | ||
pub UNNECESSARY_FOLD, | ||
Warn, | ||
"using `fold` when a more succint alternative exists" | ||
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. s/succint/succinct |
||
} | ||
|
||
impl LintPass for Pass { | ||
fn get_lints(&self) -> LintArray { | ||
lint_array!( | ||
|
@@ -653,7 +676,8 @@ impl LintPass for Pass { | |
GET_UNWRAP, | ||
STRING_EXTEND_CHARS, | ||
ITER_CLONED_COLLECT, | ||
USELESS_ASREF | ||
USELESS_ASREF, | ||
UNNECESSARY_FOLD | ||
) | ||
} | ||
} | ||
|
@@ -717,6 +741,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_unnecessary_fold(cx, expr, arglists[0]); | ||
} | ||
|
||
lint_or_fun_call(cx, expr, &method_call.name.as_str(), args); | ||
|
@@ -1105,6 +1131,93 @@ fn lint_iter_cloned_collect(cx: &LateContext, expr: &hir::Expr, iter_args: &[hir | |
} | ||
} | ||
|
||
fn lint_unnecessary_fold(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"); | ||
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. Should this actually panic? I imagine a 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. 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. |
||
|
||
fn check_fold_with_op( | ||
cx: &LateContext, | ||
fold_args: &[hir::Expr], | ||
op: hir::BinOp_, | ||
replacement_method_name: &str, | ||
replacement_has_args: bool) { | ||
|
||
if_chain! { | ||
// 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); | ||
|
||
// Check if the closure body is of the form `acc <op> some_expr(x)` | ||
if let hir::ExprBinary(ref bin_op, ref left_expr, ref right_expr) = closure_expr.node; | ||
if bin_op.node == op; | ||
|
||
// 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); | ||
|
||
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 { | ||
// Span containing `.fold(...)` | ||
let fold_span = fold_args[0].span.next_point().with_hi(fold_args[2].span.hi() + BytePos(1)); | ||
|
||
let sugg = if replacement_has_args { | ||
format!( | ||
".{replacement}(|{s}| {r})", | ||
replacement = replacement_method_name, | ||
s = second_arg_ident, | ||
r = snippet(cx, right_expr.span, "EXPR"), | ||
) | ||
} else { | ||
format!( | ||
".{replacement}()", | ||
replacement = replacement_method_name, | ||
) | ||
}; | ||
|
||
span_lint_and_sugg( | ||
cx, | ||
UNNECESSARY_FOLD, | ||
fold_span, | ||
// TODO #2371 don't suggest e.g. .any(|x| f(x)) if we can suggest .any(f) | ||
"this `.fold` can be written more succinctly using another method", | ||
"try", | ||
sugg, | ||
); | ||
} | ||
} | ||
} | ||
|
||
// Check if the first argument to .fold is a suitable literal | ||
match fold_args[1].node { | ||
hir::ExprLit(ref lit) => { | ||
match lit.node { | ||
ast::LitKind::Bool(false) => check_fold_with_op( | ||
cx, fold_args, hir::BinOp_::BiOr, "any", true | ||
), | ||
ast::LitKind::Bool(true) => check_fold_with_op( | ||
cx, fold_args, hir::BinOp_::BiAnd, "all", true | ||
), | ||
ast::LitKind::Int(0, _) => check_fold_with_op( | ||
cx, fold_args, hir::BinOp_::BiAdd, "sum", false | ||
), | ||
ast::LitKind::Int(1, _) => check_fold_with_op( | ||
cx, fold_args, hir::BinOp_::BiMul, "product", false | ||
), | ||
_ => return | ||
} | ||
} | ||
_ => return | ||
}; | ||
} | ||
|
||
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() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -385,6 +385,42 @@ fn iter_skip_next() { | |
let _ = foo.filter().skip(42).next(); | ||
} | ||
|
||
/// Calls which should trigger the `UNNECESSARY_FOLD` lint | ||
fn unnecessary_fold() { | ||
// Can be replaced by .any | ||
let _ = (0..3).fold(false, |acc, x| acc || x > 2); | ||
// Can be replaced by .all | ||
let _ = (0..3).fold(true, |acc, x| acc && x > 2); | ||
// Can be replaced by .sum | ||
let _ = (0..3).fold(0, |acc, x| acc + x); | ||
// Can be replaced by .product | ||
let _ = (0..3).fold(1, |acc, x| acc * x); | ||
} | ||
|
||
/// Should trigger the `UNNECESSARY_FOLD` lint, with an error span including exactly `.fold(...)` | ||
fn unnecessary_fold_span_for_multi_element_chain() { | ||
let _ = (0..3).map(|x| 2 * x).fold(false, |acc, x| acc || x > 2); | ||
} | ||
|
||
/// Calls which should not trigger the `UNNECESSARY_FOLD` lint | ||
fn unnecessary_fold_should_ignore() { | ||
let _ = (0..3).fold(true, |acc, x| acc || x > 2); | ||
let _ = (0..3).fold(false, |acc, x| acc && x > 2); | ||
let _ = (0..3).fold(1, |acc, x| acc + x); | ||
let _ = (0..3).fold(0, |acc, x| acc * x); | ||
let _ = (0..3).fold(0, |acc, x| 1 + acc + x); | ||
|
||
// We only match against an accumulator on the left | ||
// hand side. We could lint for .sum and .product when | ||
// it's on the right, but don't for now (and this wouldn't | ||
// be valid if we extended the lint to cover arbitrary numeric | ||
// types). | ||
let _ = (0..3).fold(false, |acc, x| x > 2 || acc); | ||
let _ = (0..3).fold(true, |acc, x| x > 2 && acc); | ||
let _ = (0..3).fold(0, |acc, x| x + acc); | ||
let _ = (0..3).fold(1, |acc, x| x * acc); | ||
} | ||
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. You might also want to add another test that doesn't trigger the lint because it's not using a boolean, e.g. 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. Will do. |
||
|
||
#[allow(similar_names)] | ||
fn main() { | ||
let opt = Some(0); | ||
|
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.
s/succint/succinct