Skip to content
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

clippy::option_if_let_else suggests hard code #8829

Open
PierreAntoineGuillaume opened this issue May 13, 2022 · 11 comments
Open

clippy::option_if_let_else suggests hard code #8829

PierreAntoineGuillaume opened this issue May 13, 2022 · 11 comments
Labels
C-bug Category: Clippy is not doing the correct thing I-false-positive Issue: The lint was triggered on code it shouldn't have L-nursery Lint: Currently in the nursery group

Comments

@PierreAntoineGuillaume
Copy link

Summary

I guess clippy::option_if_let_else should not be suggested here, because the branch yields an Output variant, and not a string slice as suggested.

Lint Name

clippy::option_if_let_else

Reproducer

I tried this code in rust playground:

#![deny(clippy::option_if_let_else)]

fn main() {
    println!("{:?}", run("ok:yay"));
    println!("{:?}", run("ko:there was an error"));
    println!("{:?}", run("crash:oh no, it crashed"));
}


#[derive(Debug)]
pub enum Output {
    Success(String, String),
    JobError(String, String),
    ProcessError(String),
}
fn run(job: &str) -> Output {
    if let Some(stripped) = job.strip_prefix("ok:") {
        Output::Success(stripped.into(), "".into())
    } else if let Some(stripped) = job.strip_prefix("ko:") {
        Output::JobError(stripped.into(), "".into())
    } else if let Some(stripped) = job.strip_prefix("crash:") {
        Output::ProcessError(stripped.into())
    } else {
        unreachable!(
            "Job should begin with ok:, ko, or crash: (actual: '{}')",
            job
        )
    }
}

I saw this happen:

error: use Option::map_or_else instead of an if let/else
   --> src/ci/job/schedule.rs:176:13
    |
176 | /             if let Some(stripped) = job.strip_prefix("ok:") {
177 | |                 Output::Success(stripped.into(), "".into())
178 | |             } else if let Some(stripped) = job.strip_prefix("ko:") {
179 | |                 Output::JobError(stripped.into(), "".into())
...   |
186 | |                 )
187 | |             }
    | |_____________^
    |
note: the lint level is defined here
   --> src/main.rs:3:9
    |
3   | #![deny(clippy::nursery)]
    |         ^^^^^^^^^^^^^^^
    = note: `#[deny(clippy::option_if_let_else)]` implied by `#[deny(clippy::nursery)]`
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
help: try
    |
176 ~             job.strip_prefix("ok:").map_or_else(|| if let Some(stripped) = job.strip_prefix("ko:") {
177 +                 Output::JobError(stripped.into(), "".into())
178 +             } else if let Some(stripped) = job.strip_prefix("crash:") {
179 +                 Output::ProcessError(stripped.into())
180 +             } else {
181 +                 unreachable!(
  ...

I expected to see this happen:
???

Version

cargo clippy --version
clippy 0.1.62 (88860d5 2022-05-09)
$ rustc -Vv
rustc 1.62.0-nightly (88860d547 2022-05-09)
binary: rustc
commit-hash: 88860d5474a32f507dde8fba8df35fd2064f11b9
commit-date: 2022-05-09
host: x86_64-unknown-linux-gnu
release: 1.62.0-nightly
LLVM version: 14.0.1

Additional Labels

No response

@PierreAntoineGuillaume PierreAntoineGuillaume added C-bug Category: Clippy is not doing the correct thing I-false-positive Issue: The lint was triggered on code it shouldn't have labels May 13, 2022
@ghost
Copy link

ghost commented May 14, 2022

You can't see the mapping argument in the suggestion because it's cut off.

I think it wants you to do this:

fn run(job: &str) -> Output {
    job.strip_prefix("ok:").map_or_else(|| 
        if let Some(stripped) = job.strip_prefix("ko:") {
             Output::JobError(stripped.into(), "".into())
        } else if let Some(stripped) = job.strip_prefix("crash:") {
             Output::ProcessError(stripped.into())
        } else {
            unreachable!(
                "Job should begin with ok:, ko, or crash: (actual: '{}')",
                job
            )
        },
        |stripped| Output::Success(stripped.into(), "".into()))
}

Once that's done it will start warning about the left over if lets.

@PierreAntoineGuillaume
Copy link
Author

So this lint suggests

fn run(&self, job: &str) -> Output {
    job.strip_prefix("ok:").map_or_else(
        || {
            job.strip_prefix("ko:").map_or_else(
                || {
                    job.strip_prefix("crash:").map_or_else(
                        || {
                            unreachable!(
                                "Job should begin with ok:, ko, or crash: (actual: '{}')",
                                job
                            )
                        },
                        |stripped| Output::ProcessError(stripped.to_string()),
                    )
                },
                |stripped| Output::JobError(stripped.to_string(), String::default()),
            )
        },
        |stripped| Output::Success(stripped.to_string(), String::default()),
    )
}

over

fn run(&self, job: &str) -> Output {
    if let Some(stripped) = job.strip_prefix("ok:") {
        Output::Success(stripped.to_string(), String::new())
    } else if let Some(stripped) = job.strip_prefix("ko:") {
        Output::JobError(stripped.to_string(), String::new())
    } else if let Some(stripped) = job.strip_prefix("crash:") {
        Output::ProcessError(stripped.to_string())
    } else {
        panic!(
            "Job should begin with ok:, ko, or crash: (actual: '{}')",
            job
        )
    }
}

And I must say I don't find the first gist nice or readable, because :

  • the second solution use 2 layers of identation, and the suggested 7
  • the second solution allows direct usage of extracted options, where the suggested forces much higher cognitive load.

First, is there a more idiomatic way of doing what I did ? (this gist is a test impl).

Second, is there a way to improve clippy ? How could I help ?

@PierreAntoineGuillaume PierreAntoineGuillaume changed the title clippy::option_if_let_else false positive when mapping to another type clippy::option_if_let_else suggests hard code May 22, 2022
@PierreAntoineGuillaume
Copy link
Author

The problem is not about a false positive, so I renamed and retag, but I don't know how to tag this

@softmoth
Copy link
Contributor

softmoth commented Sep 3, 2022

Here's a similar example, also involving strip_prefix:

        /* Clippy's suggested version, 13 lines, hard to follow, more nested */
        line.strip_prefix("fold along ").map_or_else(
            || {
                if !line.is_empty() {
                    let (a, b) = line
                        .split_once(',')
                        .expect("comma-separated pair of integers");
                    grid.insert((a.parse::<u16>().unwrap(), b.parse::<u16>().unwrap()));
                }
            },
            |fold| {
                folds.push(fold.split_once('=').unwrap());
            },
        );
        /* Original code, 8 lines, easier to follow, less indentation */
        if let Some(fold) = line.strip_prefix("fold along ") {
            folds.push(fold.split_once('=').unwrap());
        } else if !line.is_empty() {
            let (a, b) = line
                .split_once(',')
                .expect("comma-separated pair of integers");
            grid.insert((a.parse::<u16>().unwrap(), b.parse::<u16>().unwrap()));
        }

@fxdave
Copy link

fxdave commented Nov 15, 2022

I also agree

fn handle_resolve(limits: &limits::Limits, limiter_id: String, user_id: String) -> Response {
    if let Some(limiter) = limits.get_limiter(&limiter_id) {
        limiter.resolve(user_id);
        Response::ResolveDone
    } else {
        Response::ErrorLimiterNotFound
    }
}
fn handle_resolve(limits: &limits::Limits, limiter_id: String, user_id: String) -> Response {
    limits.get_limiter(&limiter_id).map_or(Response::ErrorLimiterNotFound, |limiter| {
        limiter.resolve(user_id);
        Response::ResolveDone
    })
}

The first one has less cognitive load.

However, the lint's example is good: https://rust-lang.github.io/rust-clippy/master/index.html#option_if_let_else
Somehow it should decide whether it is visually better or not.

@tgross35
Copy link
Contributor

tgross35 commented Dec 25, 2022

map_or is usually a solid improvement, but I feel like map_or_else has a tendancy to decrease readability for anything that isn't trivial - and this is kind of true for any combinators that accept two closures. Part of this is because it's not immediately clear which position is for which variant, but it also just seems to always lead to ugly formatting - all the above examples are map_or_else

Imho, it would be better to never suggest map_or_else if it means using closures, only if both arguments are function names/pointers (as suggested by redundant_closure). Or even spllit the map_or_else suggestion into restriction / not suggested at all.

The |foo| foo pattern is the suggestion is also kind of weird even though it's the most simple case:

let _ = if let Some(foo) = optional {
    foo
} else {
    let y = do_complicated_function();
    y*y
};

// suggested in the docs:
let _ = optional.map_or_else(||{
    let y = do_complicated_function();
    y*y
}, |foo| foo);

// suggested as rustfmt actually formats it - one line more than the `if let ... else`
let _ = optional.map_or_else(
    || {
        let y = do_complicated_function();
        y * y
    },
    |foo| foo,
);

The original if let ... else is definitely better. Or a match, which I personally find easier to read for simple cases over let else (adds an extra line & indent though so not true for larger block bodies)

// maybe better...?
let _ = match optional {
    Some(foo) => foo,
    None => {
        let y = do_complicated_function();
        y*y
    }
}

@HadrienG2
Copy link

In general, I would hesitate to use map_or_else when either branch is side-effectful (assertions, I/O, etc), because I that feel that obscures the control flow too much and control flow matters a lot in the presence of such operations.

@ronnodas
Copy link

map_or_else with |foo| foo is just unwrap_or_else:

// suggested in the docs:
let _ = optional.map_or_else(||{
    let y = do_complicated_function();
    y*y
}, |foo| foo);

should just be

let _ = optional.unwrap_or_else(|| {
    let y = do_complicated_function();
    y * y
});

(as formatted by rustfmt). So that's not a great example to be in the docs. Especially since there isn't a follow up lint that suggests this simplification.

@J-ZhengLi J-ZhengLi added the L-nursery Lint: Currently in the nursery group label Jul 10, 2024
@jendrikw
Copy link
Contributor

Not sure if this is relevant here or if I should raise a new issue, but I think when the else branch if just false, it better to suggest is_some_and over map_or.

Before:

if let Some(y2) = y.downcast_ref::<T>() {
    core::ptr::eq(x, y2)
} else {
    false
}

Current recommendation:

y.downcast_ref::<T>().map_or(false, |y2| core::ptr::eq(x, y2))

Alternative:

y.downcast_ref::<T>().is_some_and(|y2| core::ptr::eq(x, y2))

@jendrikw
Copy link
Contributor

Or maybe there should be a new stylistic/pedantic lint for opt.map_or(false, other_expr) in general.

@ronnodas
Copy link

Such a lint is implemented in #11796 but not merged yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: Clippy is not doing the correct thing I-false-positive Issue: The lint was triggered on code it shouldn't have L-nursery Lint: Currently in the nursery group
Projects
None yet
Development

No branches or pull requests

8 participants