Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6485,6 +6485,7 @@ Released 2018-09-13
[`option_map_unwrap_or_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_map_unwrap_or_else
[`option_option`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_option
[`option_unwrap_used`]: https://rust-lang.github.io/rust-clippy/master/index.html#option_unwrap_used
[`or_else_then_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#or_else_then_unwrap
[`or_fun_call`]: https://rust-lang.github.io/rust-clippy/master/index.html#or_fun_call
[`or_then_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#or_then_unwrap
[`out_of_bounds_indexing`]: https://rust-lang.github.io/rust-clippy/master/index.html#out_of_bounds_indexing
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
crate::methods::OPTION_AS_REF_DEREF_INFO,
crate::methods::OPTION_FILTER_MAP_INFO,
crate::methods::OPTION_MAP_OR_NONE_INFO,
crate::methods::OR_ELSE_THEN_UNWRAP_INFO,
crate::methods::OR_FUN_CALL_INFO,
crate::methods::OR_THEN_UNWRAP_INFO,
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
Expand Down
41 changes: 41 additions & 0 deletions clippy_lints/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ mod option_as_ref_cloned;
mod option_as_ref_deref;
mod option_map_or_none;
mod option_map_unwrap_or;
mod or_else_then_unwrap;
mod or_fun_call;
mod or_then_unwrap;
mod path_buf_push_overwrite;
Expand Down Expand Up @@ -916,6 +917,42 @@ declare_clippy_lint! {
"using `.chars().next()` to check if a string starts with a char"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for `.or_else(…).unwrap()` calls to Options and Results.
///
/// ### Why is this bad?
/// You should use `.unwrap_or_else(…)` instead for clarity.
///
/// ### Example
/// ```no_run
/// # let fallback = "fallback";
/// // Result
/// # type Error = &'static str;
/// # let result: Result<Vec<&str>, Error> = Err("error");
/// let value = result.or_else(|_| Ok::<Vec<&str>, Error>(vec![fallback])).unwrap();
///
/// // Option
/// # let option: Option<Vec<&str>> = None;
/// let value = option.or_else(|| Some(vec![fallback])).unwrap();
/// ```
/// Use instead:
/// ```no_run
/// # let fallback = "fallback";
/// // Result
/// # let result: Result<Vec<&str>, &str> = Err("error");
/// let value = result.unwrap_or_else(|_| vec![fallback]);
///
/// // Option
/// # let option: Option<Vec<&str>> = None;
/// let value = option.unwrap_or_else(|| vec![fallback]);
/// ```
#[clippy::version = "1.90.0"]
pub OR_ELSE_THEN_UNWRAP,
complexity,
"checks for `.or_else(…).unwrap()` calls to Options and Results."
}

declare_clippy_lint! {
/// ### What it does
/// Checks for calls to `.or(foo(..))`, `.unwrap_or(foo(..))`,
Expand Down Expand Up @@ -4680,6 +4717,7 @@ impl_lint_pass!(Methods => [
RESULT_MAP_OR_INTO_OPTION,
OPTION_MAP_OR_NONE,
BIND_INSTEAD_OF_MAP,
OR_ELSE_THEN_UNWRAP,
OR_FUN_CALL,
OR_THEN_UNWRAP,
EXPECT_FUN_CALL,
Expand Down Expand Up @@ -5534,6 +5572,9 @@ impl Methods {
Some((sym::or, recv, [or_arg], or_span, _)) => {
or_then_unwrap::check(cx, expr, recv, or_arg, or_span);
},
Some((sym::or_else, recv, [or_arg], or_span, _)) => {
or_else_then_unwrap::check(cx, expr, recv, or_arg, or_span);
},
_ => {},
}
unnecessary_literal_unwrap::check(cx, expr, recv, name, args);
Expand Down
74 changes: 74 additions & 0 deletions clippy_lints/src/methods/or_else_then_unwrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_with_applicability;
use clippy_utils::ty::is_type_diagnostic_item;
use clippy_utils::{is_res_lang_ctor, path_res};
use rustc_errors::Applicability;
use rustc_hir::lang_items::LangItem;
use rustc_hir::{Body, Expr, ExprKind};
use rustc_lint::LateContext;
use rustc_span::{Span, sym};

use super::OR_ELSE_THEN_UNWRAP;

pub(super) fn check<'tcx>(
cx: &LateContext<'tcx>,
unwrap_expr: &Expr<'_>,
recv: &'tcx Expr<'tcx>,
or_else_arg: &'tcx Expr<'_>,
or_span: Span,
) {
let ty = cx.typeck_results().expr_ty(recv); // get type of x (we later check if it's Option or Result)
let title;
let or_else_arg_content: Span;

if is_type_diagnostic_item(cx, ty, sym::Option) {
title = "found `.or_else(|| Some(…)).unwrap()`";
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::OptionSome) {
or_else_arg_content = content;
} else {
return;
}
} else if is_type_diagnostic_item(cx, ty, sym::Result) {
title = "found `.or_else(|| Ok(…)).unwrap()`";
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::ResultOk) {
or_else_arg_content = content;
} else {
return;
}
} else {
// Someone has implemented a struct with .or(...).unwrap() chaining,
// but it's not an Option or a Result, so bail
return;
}
Comment on lines +21 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_type_diagnostic_item contains a call to cx.tcx.is_diagnostic_item, which is a bit expensive. A more efficient way to do this would be to first get the DefId of ty, and then call cx.tcx.get_diagnostic_name on that.

Also, by pulling the get_content_if_ctor_matches_in_closure call into the match arm pattern, you can avoid the need to late-initalize title and or_else_arg_content

Suggested change
let title;
let or_else_arg_content: Span;
if is_type_diagnostic_item(cx, ty, sym::Option) {
title = "found `.or_else(|| Some(…)).unwrap()`";
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::OptionSome) {
or_else_arg_content = content;
} else {
return;
}
} else if is_type_diagnostic_item(cx, ty, sym::Result) {
title = "found `.or_else(|| Ok(…)).unwrap()`";
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::ResultOk) {
or_else_arg_content = content;
} else {
return;
}
} else {
// Someone has implemented a struct with .or(...).unwrap() chaining,
// but it's not an Option or a Result, so bail
return;
}
let (title, or_else_arg_content) = match ty
.ty_adt_def()
.map(AdtDef::did)
.and_then(|did| cx.tcx.get_diagnostic_name(did))
{
Some(sym::Option)
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::OptionSome) =>
{
("found `.or_else(|| Some(…)).unwrap()`", content)
},
Some(sym::Result)
if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::ResultOk) =>
{
("found `.or_else(|| Ok(…)).unwrap()`", content)
},
// Someone has implemented a struct with .or(...).unwrap() chaining,
// but it's not an Option or a Result, so bail
_ => return,
};

To avoid the rather verbose scrutinee, you could pull it into a let-chain, something like:

if let Some(did) = ty.ty_adt_def().map(AdtDef::did)
    && let Some(name) = cx.tcx.get_diagnostic_name(did)
    && let (title, or_else_arg_content) = match name {
        sym::Option
            if let Some(content) =
                get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::OptionSome) =>
        {
            ("found `.or_else(|| Some(…)).unwrap()`", content)
        },
        sym::Result
            if let Some(content) = get_content_if_ctor_matches_in_closure(cx, or_else_arg, LangItem::ResultOk) =>
        {
            ("found `.or_else(|| Ok(…)).unwrap()`", content)
        },
        // Someone has implemented a struct with .or(...).unwrap() chaining,
        // but it's not an Option or a Result, so bail
        _ => return,
    }
{
    // lint
}


let mut applicability = Applicability::MachineApplicable;
let suggestion = format!(
"unwrap_or_else(|| {})",
snippet_with_applicability(cx, or_else_arg_content, "..", &mut applicability)
);

span_lint_and_sugg(
cx,
OR_ELSE_THEN_UNWRAP,
unwrap_expr.span.with_lo(or_span.lo()),
title,
"try",
suggestion,
applicability,
);
}

fn get_content_if_ctor_matches_in_closure(cx: &LateContext<'_>, expr: &Expr<'_>, item: LangItem) -> Option<Span> {
if let ExprKind::Closure(closure) = expr.kind
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea how likely this is, but you could add a check for the Ok/Some coming from a macro expansion -- e.g., the user can't really do anything about a case like:

fn main() {
    macro_rules! some { () => { Some(5) } };
    None.or_else(|| some!()).unwrap();

See this section for more info: https://doc.rust-lang.org/clippy/development/macro_expansions.html#spanctxt-method

&& let Body {
params: [],
value: body,
} = cx.tcx.hir_body(closure.body)
&& let ExprKind::Call(some_expr, [arg]) = body.kind
&& is_res_lang_ctor(cx, path_res(cx, some_expr), item)
Comment on lines +67 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& let ExprKind::Call(some_expr, [arg]) = body.kind
&& is_res_lang_ctor(cx, path_res(cx, some_expr), item)
&& let ExprKind::Call(some_or_ok, [arg]) = body.kind
&& is_res_lang_ctor(cx, path_res(cx, some_or_ok), item)

{
Some(arg.span.source_callsite())
} else {
None
}
}
66 changes: 66 additions & 0 deletions tests/ui/or_else_then_unwrap.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#![warn(clippy::or_then_unwrap)]
#![allow(clippy::map_identity, clippy::let_unit_value, clippy::unnecessary_literal_unwrap)]

struct SomeStruct;
impl SomeStruct {
fn or_else<F: FnOnce() -> Option<Self>>(self, _: F) -> Self {
self
}
fn unwrap(&self) {}
}

struct SomeOtherStruct;
impl SomeOtherStruct {
fn or_else(self) -> Self {
self
}
fn unwrap(&self) {}
}

struct Wrapper {
inner: &'static str,
}
impl Wrapper {
fn new(inner: &'static str) -> Self {
Self { inner }
}
}

fn main() {
let option: Option<Wrapper> = None;
let _ = option.unwrap_or_else(|| Wrapper::new("fallback")); // should trigger lint
//
//~^^ or_else_then_unwrap

// as part of a method chain
let option: Option<Wrapper> = None;
let _ = option
.map(|v| v)
.unwrap_or_else(|| Wrapper::new("fallback"))
.inner
.to_string()
.chars();

// Call with macro should preserve the macro call rather than expand it
let option: Option<Vec<&'static str>> = None;
let _ = option.unwrap_or_else(|| vec!["fallback"]); // should trigger lint
//
//~^^ or_else_then_unwrap

// Not Option/Result
let instance = SomeStruct {};
let _ = instance.or_else(|| Some(SomeStruct {})).unwrap(); // should not trigger lint

// or takes no argument
let instance = SomeOtherStruct {};
let _ = instance.or_else().unwrap(); // should not trigger lint and should not panic

// None in or
let option: Option<Wrapper> = None;
#[allow(clippy::unnecessary_lazy_evaluations)]
let _ = option.or_else(|| None).unwrap(); // should not trigger lint

// other function between
let option: Option<Wrapper> = None;
let _ = option.or_else(|| Some(Wrapper::new("fallback"))).map(|v| v).unwrap(); // should not trigger lint
}
69 changes: 69 additions & 0 deletions tests/ui/or_else_then_unwrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#![warn(clippy::or_then_unwrap)]
#![allow(clippy::map_identity, clippy::let_unit_value, clippy::unnecessary_literal_unwrap)]

struct SomeStruct;
impl SomeStruct {
fn or_else<F: FnOnce() -> Option<Self>>(self, _: F) -> Self {
self
}
fn unwrap(&self) {}
}

struct SomeOtherStruct;
impl SomeOtherStruct {
fn or_else(self) -> Self {
self
}
fn unwrap(&self) {}
}

struct Wrapper {
inner: &'static str,
}
impl Wrapper {
fn new(inner: &'static str) -> Self {
Self { inner }
}
}

fn main() {
let option: Option<Wrapper> = None;
let _ = option.or_else(|| Some(Wrapper::new("fallback"))).unwrap(); // should trigger lint
//
//~^^ or_else_then_unwrap
Comment on lines +31 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having both of these comments is a bit redundant imo.. If you want to have a comment by the side, you could do something like:

Suggested change
let _ = option.or_else(|| Some(Wrapper::new("fallback"))).unwrap(); // should trigger lint
//
//~^^ or_else_then_unwrap
let _ = option.or_else(|| Some(Wrapper::new("fallback"))).unwrap(); //~ or_else_then_unwrap

Which is not very common across the test suite, but pretty clean imo


// as part of a method chain
let option: Option<Wrapper> = None;
let _ = option
.map(|v| v)
.or_else(|| Some(Wrapper::new("fallback"))) // should trigger lint
//
//~^^ or_else_then_unwrap
.unwrap()
.inner
.to_string()
.chars();

// Call with macro should preserve the macro call rather than expand it
let option: Option<Vec<&'static str>> = None;
let _ = option.or_else(|| Some(vec!["fallback"])).unwrap(); // should trigger lint
//
//~^^ or_else_then_unwrap

// Not Option/Result
let instance = SomeStruct {};
let _ = instance.or_else(|| Some(SomeStruct {})).unwrap(); // should not trigger lint

// or takes no argument
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a bit to parse this 😅 Could you please add backticks, and specify the whole name while we're at it?

Suggested change
// or takes no argument
// `or_else` takes no argument

let instance = SomeOtherStruct {};
let _ = instance.or_else().unwrap(); // should not trigger lint and should not panic

// None in or
let option: Option<Wrapper> = None;
#[allow(clippy::unnecessary_lazy_evaluations)]
let _ = option.or_else(|| None).unwrap(); // should not trigger lint

// other function between
let option: Option<Wrapper> = None;
let _ = option.or_else(|| Some(Wrapper::new("fallback"))).map(|v| v).unwrap(); // should not trigger lint
}
26 changes: 26 additions & 0 deletions tests/ui/or_else_then_unwrap.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
error: found `.or_else(|| Some(…)).unwrap()`
--> tests/ui/or_else_then_unwrap.rs:31:20
|
LL | let _ = option.or_else(|| Some(Wrapper::new("fallback"))).unwrap(); // should trigger lint
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| Wrapper::new("fallback"))`
|
= note: `-D clippy::or-else-then-unwrap` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::or_else_then_unwrap)]`

error: found `.or_else(|| Some(…)).unwrap()`
--> tests/ui/or_else_then_unwrap.rs:39:10
|
LL | .or_else(|| Some(Wrapper::new("fallback"))) // should trigger lint
| __________^
... |
LL | | .unwrap()
| |_________________^ help: try: `unwrap_or_else(|| Wrapper::new("fallback"))`
Comment on lines +11 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diagnostic/suggestion looks a bit crowded -- could you try replacing this:

span_lint_and_sugg(
    cx,
    OR_ELSE_THEN_UNWRAP,
    unwrap_expr.span.with_lo(or_span.lo()),
    title,
    "try",
    suggestion,
    applicability,
);

with span_lint_and_then containing a call to Diag::span_suggestion_verbose?


error: found `.or_else(|| Some(…)).unwrap()`
--> tests/ui/or_else_then_unwrap.rs:49:20
|
LL | let _ = option.or_else(|| Some(vec!["fallback"])).unwrap(); // should trigger lint
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `unwrap_or_else(|| vec!["fallback"])`

error: aborting due to 3 previous errors