about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-07-26 03:13:30 +0000
committerbors <bors@rust-lang.org>2023-07-26 03:13:30 +0000
commit0fec99f8f8d5d76650dfebad4e31e22408e0e3dd (patch)
treed99b08c51a78a6693439119385205728ecffdc60
parent2153c0fcc893534d61b4d66de80e615e7bff6017 (diff)
parent3235d9d612909bc64550eea3a0d387e1187e93dd (diff)
downloadrust-0fec99f8f8d5d76650dfebad4e31e22408e0e3dd.tar.gz
rust-0fec99f8f8d5d76650dfebad4e31e22408e0e3dd.zip
Auto merge of #11115 - Centri3:filter_map_bool_then, r=Jarcho
New lint [`filter_map_bool_then`]

Closes #9098

changelog: New lint [`filter_map_bool_then`]
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/methods/filter_map_bool_then.rs45
-rw-r--r--clippy_lints/src/methods/mod.rs34
-rw-r--r--clippy_utils/src/paths.rs2
-rw-r--r--tests/ui/filter_map_bool_then.fixed44
-rw-r--r--tests/ui/filter_map_bool_then.rs44
-rw-r--r--tests/ui/filter_map_bool_then.stderr34
8 files changed, 205 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02d12d60e73..251f32d2e6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4844,6 +4844,7 @@ Released 2018-09-13
 [`field_reassign_with_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#field_reassign_with_default
 [`filetype_is_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#filetype_is_file
 [`filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map
+[`filter_map_bool_then`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_bool_then
 [`filter_map_identity`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_identity
 [`filter_map_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_next
 [`filter_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_next
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 9b99a255423..e084471433d 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -337,6 +337,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     crate::methods::EXPECT_USED_INFO,
     crate::methods::EXTEND_WITH_DRAIN_INFO,
     crate::methods::FILETYPE_IS_FILE_INFO,
+    crate::methods::FILTER_MAP_BOOL_THEN_INFO,
     crate::methods::FILTER_MAP_IDENTITY_INFO,
     crate::methods::FILTER_MAP_NEXT_INFO,
     crate::methods::FILTER_NEXT_INFO,
diff --git a/clippy_lints/src/methods/filter_map_bool_then.rs b/clippy_lints/src/methods/filter_map_bool_then.rs
new file mode 100644
index 00000000000..4aee22a4afc
--- /dev/null
+++ b/clippy_lints/src/methods/filter_map_bool_then.rs
@@ -0,0 +1,45 @@
+use clippy_utils::diagnostics::span_lint_and_sugg;
+use clippy_utils::paths::BOOL_THEN;
+use clippy_utils::source::snippet_opt;
+use clippy_utils::ty::is_copy;
+use clippy_utils::{is_from_proc_macro, is_trait_method, match_def_path, peel_blocks};
+use rustc_errors::Applicability;
+use rustc_hir::{Expr, ExprKind};
+use rustc_lint::{LateContext, LintContext};
+use rustc_middle::lint::in_external_macro;
+use rustc_span::{sym, Span};
+
+use super::FILTER_MAP_BOOL_THEN;
+
+pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, arg: &Expr<'_>, call_span: Span) {
+    if !in_external_macro(cx.sess(), expr.span)
+        && is_trait_method(cx, expr, sym::Iterator)
+        && let ExprKind::Closure(closure) = arg.kind
+        && let body = cx.tcx.hir().body(closure.body)
+        && let value = peel_blocks(body.value)
+        // Indexing should be fine as `filter_map` always has 1 input, we unfortunately need both
+        // `inputs` and `params` here as we need both the type and the span
+        && let param_ty = closure.fn_decl.inputs[0]
+        && let param = body.params[0]
+        && is_copy(cx, cx.typeck_results().node_type(param_ty.hir_id).peel_refs())
+        && let ExprKind::MethodCall(_, recv, [then_arg], _) = value.kind
+        && let ExprKind::Closure(then_closure) = then_arg.kind
+        && let then_body = peel_blocks(cx.tcx.hir().body(then_closure.body).value)
+        && let Some(def_id) = cx.typeck_results().type_dependent_def_id(value.hir_id)
+        && match_def_path(cx, def_id, &BOOL_THEN)
+        && !is_from_proc_macro(cx, expr)
+        && let Some(param_snippet) = snippet_opt(cx, param.span)
+        && let Some(filter) = snippet_opt(cx, recv.span)
+        && let Some(map) = snippet_opt(cx, then_body.span)
+    {
+        span_lint_and_sugg(
+            cx,
+            FILTER_MAP_BOOL_THEN,
+            call_span,
+            "usage of `bool::then` in `filter_map`",
+            "use `filter` then `map` instead",
+            format!("filter(|&{param_snippet}| {filter}).map(|{param_snippet}| {map})"),
+            Applicability::MachineApplicable,
+        );
+    }
+}
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 1058289679b..e774450018e 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -21,6 +21,7 @@ mod expect_used;
 mod extend_with_drain;
 mod filetype_is_file;
 mod filter_map;
+mod filter_map_bool_then;
 mod filter_map_identity;
 mod filter_map_next;
 mod filter_next;
@@ -3475,6 +3476,37 @@ declare_clippy_lint! {
     "disallows `.skip(0)`"
 }
 
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for usage of `bool::then` in `Iterator::filter_map`.
+    ///
+    /// ### Why is this bad?
+    /// This can be written with `filter` then `map` instead, which would reduce nesting and
+    /// separates the filtering from the transformation phase. This comes with no cost to
+    /// performance and is just cleaner.
+    ///
+    /// ### Limitations
+    /// Does not lint `bool::then_some`, as it eagerly evaluates its arguments rather than lazily.
+    /// This can create differing behavior, so better safe than sorry.
+    ///
+    /// ### Example
+    /// ```rust
+    /// # fn really_expensive_fn(i: i32) -> i32 { i }
+    /// # let v = vec![];
+    /// _ = v.into_iter().filter_map(|i| (i % 2 == 0).then(|| really_expensive_fn(i)));
+    /// ```
+    /// Use instead:
+    /// ```rust
+    /// # fn really_expensive_fn(i: i32) -> i32 { i }
+    /// # let v = vec![];
+    /// _ = v.into_iter().filter(|i| i % 2 == 0).map(|i| really_expensive_fn(i));
+    /// ```
+    #[clippy::version = "1.72.0"]
+    pub FILTER_MAP_BOOL_THEN,
+    style,
+    "checks for usage of `bool::then` in `Iterator::filter_map`"
+}
+
 pub struct Methods {
     avoid_breaking_exported_api: bool,
     msrv: Msrv,
@@ -3612,6 +3644,7 @@ impl_lint_pass!(Methods => [
     FORMAT_COLLECT,
     STRING_LIT_CHARS_ANY,
     ITER_SKIP_ZERO,
+    FILTER_MAP_BOOL_THEN,
 ]);
 
 /// Extracts a method call name, args, and `Span` of the method name.
@@ -3889,6 +3922,7 @@ impl Methods {
                 },
                 ("filter_map", [arg]) => {
                     unnecessary_filter_map::check(cx, expr, arg, name);
+                    filter_map_bool_then::check(cx, expr, arg, call_span);
                     filter_map_identity::check(cx, expr, arg, span);
                 },
                 ("find_map", [arg]) => {
diff --git a/clippy_utils/src/paths.rs b/clippy_utils/src/paths.rs
index 8d96d3cfe50..914ea85ac28 100644
--- a/clippy_utils/src/paths.rs
+++ b/clippy_utils/src/paths.rs
@@ -163,3 +163,5 @@ pub const OPTION_EXPECT: [&str; 4] = ["core", "option", "Option", "expect"];
 pub const FORMATTER: [&str; 3] = ["core", "fmt", "Formatter"];
 pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];
 pub const ORD_CMP: [&str; 4] = ["core", "cmp", "Ord", "cmp"];
+#[expect(clippy::invalid_paths)] // not sure why it thinks this, it works so
+pub const BOOL_THEN: [&str; 4] = ["core", "bool", "<impl bool>", "then"];
diff --git a/tests/ui/filter_map_bool_then.fixed b/tests/ui/filter_map_bool_then.fixed
new file mode 100644
index 00000000000..3e72fee4b07
--- /dev/null
+++ b/tests/ui/filter_map_bool_then.fixed
@@ -0,0 +1,44 @@
+//@run-rustfix
+//@aux-build:proc_macros.rs:proc-macro
+#![allow(
+    clippy::clone_on_copy,
+    clippy::map_identity,
+    clippy::unnecessary_lazy_evaluations,
+    unused
+)]
+#![warn(clippy::filter_map_bool_then)]
+
+#[macro_use]
+extern crate proc_macros;
+
+#[derive(Clone, PartialEq)]
+struct NonCopy;
+
+fn main() {
+    let v = vec![1, 2, 3, 4, 5, 6];
+    v.clone().iter().filter(|&i| (i % 2 == 0)).map(|i| i + 1);
+    v.clone().into_iter().filter(|&i| (i % 2 == 0)).map(|i| i + 1);
+    v.clone()
+        .into_iter()
+        .filter(|&i| (i % 2 == 0)).map(|i| i + 1);
+    v.clone()
+        .into_iter()
+        .filter(|&i| i != 1000)
+        .filter(|&i| (i % 2 == 0)).map(|i| i + 1);
+    v.iter()
+        .copied()
+        .filter(|&i| i != 1000)
+        .filter(|&i| (i.clone() % 2 == 0)).map(|i| i + 1);
+    // Do not lint
+    let v = vec![NonCopy, NonCopy];
+    v.clone().iter().filter_map(|i| (i == &NonCopy).then(|| i));
+    external! {
+        let v = vec![1, 2, 3, 4, 5, 6];
+        v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    }
+    with_span! {
+        span
+        let v = vec![1, 2, 3, 4, 5, 6];
+        v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    }
+}
diff --git a/tests/ui/filter_map_bool_then.rs b/tests/ui/filter_map_bool_then.rs
new file mode 100644
index 00000000000..38a04e57de4
--- /dev/null
+++ b/tests/ui/filter_map_bool_then.rs
@@ -0,0 +1,44 @@
+//@run-rustfix
+//@aux-build:proc_macros.rs:proc-macro
+#![allow(
+    clippy::clone_on_copy,
+    clippy::map_identity,
+    clippy::unnecessary_lazy_evaluations,
+    unused
+)]
+#![warn(clippy::filter_map_bool_then)]
+
+#[macro_use]
+extern crate proc_macros;
+
+#[derive(Clone, PartialEq)]
+struct NonCopy;
+
+fn main() {
+    let v = vec![1, 2, 3, 4, 5, 6];
+    v.clone().iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    v.clone()
+        .into_iter()
+        .filter_map(|i| -> Option<_> { (i % 2 == 0).then(|| i + 1) });
+    v.clone()
+        .into_iter()
+        .filter(|&i| i != 1000)
+        .filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    v.iter()
+        .copied()
+        .filter(|&i| i != 1000)
+        .filter_map(|i| (i.clone() % 2 == 0).then(|| i + 1));
+    // Do not lint
+    let v = vec![NonCopy, NonCopy];
+    v.clone().iter().filter_map(|i| (i == &NonCopy).then(|| i));
+    external! {
+        let v = vec![1, 2, 3, 4, 5, 6];
+        v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    }
+    with_span! {
+        span
+        let v = vec![1, 2, 3, 4, 5, 6];
+        v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+    }
+}
diff --git a/tests/ui/filter_map_bool_then.stderr b/tests/ui/filter_map_bool_then.stderr
new file mode 100644
index 00000000000..b411cd83dfd
--- /dev/null
+++ b/tests/ui/filter_map_bool_then.stderr
@@ -0,0 +1,34 @@
+error: usage of `bool::then` in `filter_map`
+  --> $DIR/filter_map_bool_then.rs:19:22
+   |
+LL |     v.clone().iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
+   |
+   = note: `-D clippy::filter-map-bool-then` implied by `-D warnings`
+
+error: usage of `bool::then` in `filter_map`
+  --> $DIR/filter_map_bool_then.rs:20:27
+   |
+LL |     v.clone().into_iter().filter_map(|i| (i % 2 == 0).then(|| i + 1));
+   |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
+
+error: usage of `bool::then` in `filter_map`
+  --> $DIR/filter_map_bool_then.rs:23:10
+   |
+LL |         .filter_map(|i| -> Option<_> { (i % 2 == 0).then(|| i + 1) });
+   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
+
+error: usage of `bool::then` in `filter_map`
+  --> $DIR/filter_map_bool_then.rs:27:10
+   |
+LL |         .filter_map(|i| (i % 2 == 0).then(|| i + 1));
+   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i % 2 == 0)).map(|i| i + 1)`
+
+error: usage of `bool::then` in `filter_map`
+  --> $DIR/filter_map_bool_then.rs:31:10
+   |
+LL |         .filter_map(|i| (i.clone() % 2 == 0).then(|| i + 1));
+   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use `filter` then `map` instead: `filter(|&i| (i.clone() % 2 == 0)).map(|i| i + 1)`
+
+error: aborting due to 5 previous errors
+