-
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 8 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,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!( | ||
|
@@ -653,7 +670,8 @@ impl LintPass for Pass { | |
GET_UNWRAP, | ||
STRING_EXTEND_CHARS, | ||
ITER_CLONED_COLLECT, | ||
USELESS_ASREF | ||
USELESS_ASREF, | ||
FOLD_ANY | ||
) | ||
} | ||
} | ||
|
@@ -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); | ||
|
@@ -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"); | ||
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. |
||
|
||
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) | ||
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. 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() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
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. replace |
||
pub fn span_lint_and_sugg<'a, 'tcx: 'a, T: LintContext<'tcx>>( | ||
cx: &'a T, | ||
lint: &'static Lint, | ||
|
@@ -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, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
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. |
||
|
||
/// 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); | ||
|
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.
Nit: Does this macro support trailing commas? If so, please add one and the next diff is only one line :)
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.
It appears not:
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.
Ugh, that's too bad. Someone should make a pull request to change this :)
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've created an issue #2352. I'll do this next weekend if it's still open.
Uh oh!
There was an error while loading. Please reload this page.
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.
Oh, this is a macro defined in the compiler, so that issue is probably more useful over at https://github.com/rust-lang/rust :)
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.
Nuts. rust-lang/rust#47428