Repository: rust-lang/rust-clippy
Pull request #14358: suggest `is_some_and` instead of `map_or` in `case_sensitive_file_extension_comparions`
Author: login: lapla-cogito
Labels: name: S-waiting-on-review
description: Status: Awaiting review from the assignee but also interested parties
Created: 2025-03-06T04:54:18Z | Merged: 2025-03-25T14:55:30Z

close #14357

changelog: [`case_sensitive_file_extension_comparisons`]: suggest `is_some_and` to suppress other lint warnings


Linked issue #14357: case_sensitive_file_extension_comparisons should suggest `is_some_and`
### Summary

The `clippy::case_sensitive_file_extension_comparisons` suggestion should not cause subsequent clippy lint.

### Reproducer

I tried this code:

```rust
fn foo(v: &str)->bool {
    v.ends_with(".a")
}

fn main() {
    println!("test = {:?}", foo("test.a"));
}
```

I expected the suggestion to not cause any clippy warnings, but instead, it suggested to replace it with this:

```rust
2 ~     std::path::Path::new(v)
3 +         .extension()
4 +         .map_or(false, |ext| ext.eq_ignore_ascii_case("a"))
 ```

The suggested code produces this:

```rust
  = note: `#[warn(clippy::unnecessary_map_or)]` on by default
help: use is_some_and instead
  |
2 ~     std::path::Path::new(v)
3 +         .extension().is_some_and(|ext| ext.eq_ignore_ascii_case("a"))
  |
```

### Version

```text
rustc 1.85.0 (4d91de4e4 2025-02-17)
binary: rustc
commit-hash: 4d91de4e48198da2e33413efdcd9cd2cc0c46688
commit-date: 2025-02-17
host: x86_64-unknown-linux-gnu
release: 1.85.0
LLVM version: 19.1.7
```

### Additional Labels

_No response_

Diff:
diff --git a/clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs b/clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
index 18568e3661fe..1c745b7376ea 100644
--- a/clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
+++ b/clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
@@ -1,4 +1,5 @@
 use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::msrvs::{self, Msrv};
 use clippy_utils::source::{SpanRangeExt, indent_of, reindent_multiline};
 use clippy_utils::ty::is_type_lang_item;
 use rustc_ast::ast::LitKind;
@@ -16,6 +17,7 @@ pub(super) fn check<'tcx>(
     call_span: Span,
     recv: &'tcx Expr<'_>,
     arg: &'tcx Expr<'_>,
+    msrv: Msrv,
 ) {
     if let ExprKind::MethodCall(path_segment, ..) = recv.kind {
         if matches!(
@@ -58,11 +60,15 @@ pub(super) fn check<'tcx>(
 
                     let suggestion_source = reindent_multiline(
                         &format!(
-                            "std::path::Path::new({})
+                            "std::path::Path::new({recv_source})
                                 .extension()
-                                .map_or(false, |ext| ext.eq_ignore_ascii_case(\"{}\"))",
-                            recv_source,
-                            ext_str.strip_prefix('.').unwrap()
+                                .{}|ext| ext.eq_ignore_ascii_case(\"{}\"))",
+                            if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                                "is_some_and("
+                            } else {
+                                "map_or(false, "
+                            },
+                            ext_str.strip_prefix('.').unwrap(),
                         ),
                         true,
                         Some(indent_of(cx, call_span).unwrap_or(0) + 4),
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 94d3657d9f12..f17bf6d63662 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -4992,7 +4992,7 @@ impl Methods {
                 },
                 ("ends_with", [arg]) => {
                     if let ExprKind::MethodCall(.., span) = expr.kind {
-                        case_sensitive_file_extension_comparisons::check(cx, expr, span, recv, arg);
+                        case_sensitive_file_extension_comparisons::check(cx, expr, span, recv, arg, self.msrv);
                     }
                     path_ends_with_ext::check(cx, recv, arg, expr, self.msrv, &self.allowed_dotfiles);
                 },
diff --git a/tests/ui/case_sensitive_file_extension_comparisons.fixed b/tests/ui/case_sensitive_file_extension_comparisons.fixed
index bf7635fdf09b..0c9d21243546 100644
--- a/tests/ui/case_sensitive_file_extension_comparisons.fixed
+++ b/tests/ui/case_sensitive_file_extension_comparisons.fixed
@@ -1,5 +1,4 @@
 #![warn(clippy::case_sensitive_file_extension_comparisons)]
-#![allow(clippy::unnecessary_map_or)]
 
 use std::string::String;
 
@@ -13,7 +12,7 @@ impl TestStruct {
 fn is_rust_file(filename: &str) -> bool {
     std::path::Path::new(filename)
         .extension()
-        .map_or(false, |ext| ext.eq_ignore_ascii_case("rs"))
+        .is_some_and(|ext| ext.eq_ignore_ascii_case("rs"))
     //~^ case_sensitive_file_extension_comparisons
 }
 
@@ -21,18 +20,18 @@ fn main() {
     // std::string::String and &str should trigger the lint failure with .ext12
     let _ = std::path::Path::new(&String::new())
         .extension()
-        .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+        .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
     //~^ case_sensitive_file_extension_comparisons
     let _ = std::path::Path::new("str")
         .extension()
-        .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+        .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
     //~^ case_sensitive_file_extension_comparisons
 
     // The fixup should preserve the indentation level
     {
         let _ = std::path::Path::new("str")
             .extension()
-            .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+            .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
         //~^ case_sensitive_file_extension_comparisons
     }
 
@@ -42,11 +41,11 @@ fn main() {
     // std::string::String and &str should trigger the lint failure with .EXT12
     let _ = std::path::Path::new(&String::new())
         .extension()
-        .map_or(false, |ext| ext.eq_ignore_ascii_case("EXT12"));
+        .is_some_and(|ext| ext.eq_ignore_ascii_case("EXT12"));
     //~^ case_sensitive_file_extension_comparisons
     let _ = std::path::Path::new("str")
         .extension()
-        .map_or(false, |ext| ext.eq_ignore_ascii_case("EXT12"));
+        .is_some_and(|ext| ext.eq_ignore_ascii_case("EXT12"));
     //~^ case_sensitive_file_extension_comparisons
 
     // Should not trigger the lint failure because of the calls to to_lowercase and to_uppercase
@@ -76,3 +75,11 @@ fn main() {
     let _ = "str".ends_with(".123");
     TestStruct {}.ends_with(".123");
 }
+
+#[clippy::msrv = "1.69"]
+fn msrv_check() {
+    let _ = std::path::Path::new(&String::new())
+        .extension()
+        .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+    //~^ case_sensitive_file_extension_comparisons
+}
diff --git a/tests/ui/case_sensitive_file_extension_comparisons.rs b/tests/ui/case_sensitive_file_extension_comparisons.rs
index 0c4070a42d4b..f8a947aa827b 100644
--- a/tests/ui/case_sensitive_file_extension_comparisons.rs
+++ b/tests/ui/case_sensitive_file_extension_comparisons.rs
@@ -1,5 +1,4 @@
 #![warn(clippy::case_sensitive_file_extension_comparisons)]
-#![allow(clippy::unnecessary_map_or)]
 
 use std::string::String;
 
@@ -64,3 +63,9 @@ fn main() {
     let _ = "str".ends_with(".123");
     TestStruct {}.ends_with(".123");
 }
+
+#[clippy::msrv = "1.69"]
+fn msrv_check() {
+    let _ = String::new().ends_with(".ext12");
+    //~^ case_sensitive_file_extension_comparisons
+}
diff --git a/tests/ui/case_sensitive_file_extension_comparisons.stderr b/tests/ui/case_sensitive_file_extension_comparisons.stderr
index e035534d2699..93bee8e76671 100644
--- a/tests/ui/case_sensitive_file_extension_comparisons.stderr
+++ b/tests/ui/case_sensitive_file_extension_comparisons.stderr
@@ -1,5 +1,5 @@
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:14:5
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:13:5
    |
 LL |     filename.ends_with(".rs")
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -11,11 +11,11 @@ help: use std::path::Path
    |
 LL ~     std::path::Path::new(filename)
 LL +         .extension()
-LL +         .map_or(false, |ext| ext.eq_ignore_ascii_case("rs"))
+LL +         .is_some_and(|ext| ext.eq_ignore_ascii_case("rs"))
    |
 
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:20:13
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:19:13
    |
 LL |     let _ = String::new().ends_with(".ext12");
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -25,11 +25,11 @@ help: use std::path::Path
    |
 LL ~     let _ = std::path::Path::new(&String::new())
 LL +         .extension()
-LL ~         .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+LL ~         .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
    |
 
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:22:13
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:21:13
    |
 LL |     let _ = "str".ends_with(".ext12");
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -39,11 +39,11 @@ help: use std::path::Path
    |
 LL ~     let _ = std::path::Path::new("str")
 LL +         .extension()
-LL ~         .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+LL ~         .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
    |
 
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:27:17
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:26:17
    |
 LL |         let _ = "str".ends_with(".ext12");
    |                 ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -53,11 +53,11 @@ help: use std::path::Path
    |
 LL ~         let _ = std::path::Path::new("str")
 LL +             .extension()
-LL ~             .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
+LL ~             .is_some_and(|ext| ext.eq_ignore_ascii_case("ext12"));
    |
 
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:35:13
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:34:13
    |
 LL |     let _ = String::new().ends_with(".EXT12");
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -67,11 +67,11 @@ help: use std::path::Path
    |
 LL ~     let _ = std::path::Path::new(&String::new())
 LL +         .extension()
-LL ~         .map_or(false, |ext| ext.eq_ignore_ascii_case("EXT12"));
+LL ~         .is_some_and(|ext| ext.eq_ignore_ascii_case("EXT12"));
    |
 
 error: case-sensitive file extension comparison
-  --> tests/ui/case_sensitive_file_extension_comparisons.rs:37:13
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:36:13
    |
 LL |     let _ = "str".ends_with(".EXT12");
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -81,8 +81,22 @@ help: use std::path::Path
    |
 LL ~     let _ = std::path::Path::new("str")
 LL +         .extension()
-LL ~         .map_or(false, |ext| ext.eq_ignore_ascii_case("EXT12"));
+LL ~         .is_some_and(|ext| ext.eq_ignore_ascii_case("EXT12"));
+   |
+
+error: case-sensitive file extension comparison
+  --> tests/ui/case_sensitive_file_extension_comparisons.rs:69:13
+   |
+LL |     let _ = String::new().ends_with(".ext12");
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+   |
+   = help: consider using a case-insensitive comparison instead
+help: use std::path::Path
+   |
+LL ~     let _ = std::path::Path::new(&String::new())
+LL +         .extension()
+LL ~         .map_or(false, |ext| ext.eq_ignore_ascii_case("ext12"));
    |
 
-error: aborting due to 6 previous errors
+error: aborting due to 7 previous errors
 


Reviews:
user: login: Centri3
body: This changes the MSRV of this lint, 1.0.0 -> 1.70.0. We don't want to bump the MSRV just for the suggestion, so please only make this change apply with >1.70.0.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-06T05:24:47Z
user: login: Centri3
body: That's pretty common in tests - Any warning in `.fixed` raises an error. It's not really ideal but if it's there it's whatever - What I'm talking about instead is whether they'd be linted if these cases were in user code.

If it takes a few runs for clippy to reach homeostatis, that's fine and expected, but reducing this is always accepted :+1:
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T21:19:15Z
user: login: lapla-cogito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T00:33:11Z
user: login: Centri3
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-07T06:57:48Z
user: login: lapla-cogito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T07:45:24Z
user: login: samueltardieu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T09:26:25Z
user: login: lapla-cogito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:29:30Z
user: login: samueltardieu
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T12:51:21Z
user: login: lapla-cogito
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:17:55Z
user: login: samueltardieu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T14:47:47Z

Review comments:
diff_hunk: @@ -56,13 +58,23 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
+                            ".is_some_and(|ext| ext.eq_ignore_ascii_case(\"{}\"))",
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: Centri3
body: I feel like this should only be the prefix, which doesn't need to be `format!` anyways.

Note I think` format_args!` would usually be applicable here but it won't be needed once this change is applied.
created_at: 2025-03-06T21:18:02Z
author_association: MEMBER
diff_hunk: @@ -56,13 +58,23 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
+                            ".is_some_and(|ext| ext.eq_ignore_ascii_case(\"{}\"))",
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: lapla-cogito
body: It's not necessary indeed, but I did it this way for code readability reasons (it's hard to read when balanced parentheses literals are scattered in different places).
created_at: 2025-03-07T00:33:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,13 +58,23 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
+                            ".is_some_and(|ext| ext.eq_ignore_ascii_case(\"{}\"))",
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: Centri3
body: I mentioned the possibility in an earlier iteration, in the case of it being for readability (in your opinion) then I could merge anyways.

I would look at `format_args!` rq but that's it
created_at: 2025-03-07T06:57:46Z
author_association: MEMBER
diff_hunk: @@ -56,13 +58,29 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: lapla-cogito
body: @Centri3 I might be missing something, but `format_args!` creates a temporary value, and using it later requires evaluating it as a string. So I'm wondering what you intend...
created_at: 2025-03-08T07:45:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,13 +58,29 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: samueltardieu
body: You could use the following (which is shorter) to avoid multiple string allocations:

```rust
                    let suggestion_source = reindent_multiline(
                        &format!(
                            "std::path::Path::new({recv_source})
                                .extension()
                                .{}|ext| ext.eq_ignore_ascii_case(\"{}\"))",
                            if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
                                "is_some_and("
                            } else {
                                "map_or(false, "
                            },
                            ext_str.strip_prefix('.').unwrap(),
                        ),
                        true,
                        Some(indent_of(cx, call_span).unwrap_or(0) + 4),
                    );
```
created_at: 2025-03-25T09:26:25Z
author_association: MEMBER
diff_hunk: @@ -56,13 +58,29 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: lapla-cogito
body: Thanks! This should be all good now.
created_at: 2025-03-25T12:29:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,13 +58,29 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: lapla-cogito
body: @samueltardieu
If this patch looks good, could you merge it on behalf of Catharine? Since Catharine is not taking new assignments at the moment, it's unlikely that this patch will be reviewed again soon.
created_at: 2025-03-25T13:17:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,13 +58,29 @@ pub(super) fn check<'tcx>(
                         format!("&{recv_source}")
                     };
 
+                    let last_method = if msrv.meets(cx, msrvs::OPTION_RESULT_IS_VARIANT_AND) {
+                        format!(
path: clippy_lints/src/methods/case_sensitive_file_extension_comparisons.rs
user: login: samueltardieu
body: Since they approved it with only one nit which has now been addressed, I will gladly remove this from their (well loaded) plate.
created_at: 2025-03-25T14:47:47Z
author_association: MEMBER

PR comments:
user: login: rustbot
created_at: 2025-03-06T04:54:23Z
body: r? @Centri3

rustbot has assigned @Centri3.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use `r?` to explicitly pick a reviewer
author_association: COLLABORATOR
user: login: lapla-cogito
created_at: 2025-03-06T05:03:52Z
body: I'll make this as draft because some adjustments (ex: MSRV) are needed. (Sorry I didn't notice that first.)
author_association: CONTRIBUTOR
user: login: Centri3
created_at: 2025-03-06T05:25:24Z
body: > (ex: MSRV)

Didn't see this. Yes, exactly.
author_association: MEMBER
user: login: Centri3
created_at: 2025-03-06T05:26:46Z
body: Question: You say this is to suppress lint warnings in the changelog. Does clippy already catch all of these cases with a separate lint (that I do remember) after applying the first suggestion?
author_association: MEMBER
user: login: lapla-cogito
created_at: 2025-03-06T06:15:00Z
body: @Centri3 

> Question: You say this is to suppress lint warnings in the changelog. Does clippy already catch all of these cases with a separate lint (that I do remember) after applying the first suggestion?

No, it's because `unnecessary_map_or` was explicitly allowed in the test file.
author_association: CONTRIBUTOR
user: login: Centri3
created_at: 2025-03-08T06:55:27Z
body: Just a little nudge that I'm happy merging right now if you've looked at whether `format_args!` works
author_association: MEMBER
user: login: lapla-cogito
created_at: 2025-03-08T07:06:31Z
body: @Centri3 Rewrote this using `format_args!`, thanks.
author_association: CONTRIBUTOR
user: login: Centri3
created_at: 2025-03-08T07:14:11Z
body: Do note, I meant replacing the entire two `format!`s with `format_args!` :sweat_smile: Having them wrapped in a `format!` has the same issue.
Passing a `String` from `format!` to another `format!` can generally be optimized using `format_args!` without lifetime shenanigans. If there *are* lifetime shenanigans then `format!` is the only option
author_association: MEMBER