Skip to content

Commit c43abbf

Browse files
committed
New lint: manual_ok_err
1 parent 650e0c8 commit c43abbf

File tree

7 files changed

+429
-0
lines changed

7 files changed

+429
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5717,6 +5717,7 @@ Released 2018-09-13
57175717
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
57185718
[`manual_next_back`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_next_back
57195719
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
5720+
[`manual_ok_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_err
57205721
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
57215722
[`manual_pattern_char_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
57225723
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
331331
crate::matches::INFALLIBLE_DESTRUCTURING_MATCH_INFO,
332332
crate::matches::MANUAL_FILTER_INFO,
333333
crate::matches::MANUAL_MAP_INFO,
334+
crate::matches::MANUAL_OK_ERR_INFO,
334335
crate::matches::MANUAL_UNWRAP_OR_INFO,
335336
crate::matches::MATCH_AS_REF_INFO,
336337
crate::matches::MATCH_BOOL_INFO,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::sugg::Sugg;
3+
use clippy_utils::{is_res_lang_ctor, path_res, peel_blocks, span_contains_comment};
4+
use rustc_ast::BindingMode;
5+
use rustc_errors::Applicability;
6+
use rustc_hir::LangItem::{OptionNone, OptionSome};
7+
use rustc_hir::def::{DefKind, Res};
8+
use rustc_hir::{Arm, Expr, ExprKind, Pat, PatKind, Path, QPath};
9+
use rustc_lint::{LateContext, LintContext};
10+
use rustc_span::symbol::Ident;
11+
12+
use super::MANUAL_OK_ERR;
13+
14+
pub(crate) fn check_if_let(
15+
cx: &LateContext<'_>,
16+
expr: &Expr<'_>,
17+
let_pat: &Pat<'_>,
18+
let_expr: &Expr<'_>,
19+
if_then: &Expr<'_>,
20+
else_expr: &Expr<'_>,
21+
) {
22+
if let Some((is_ok, ident)) = is_ok_or_err(cx, let_pat)
23+
&& is_some_ident(cx, if_then, ident)
24+
&& is_none(cx, else_expr)
25+
{
26+
apply_lint(cx, expr, let_expr, is_ok);
27+
}
28+
}
29+
30+
pub(crate) fn check_match(cx: &LateContext<'_>, expr: &Expr<'_>, scrutinee: &Expr<'_>, arms: &[Arm<'_>]) {
31+
if arms.len() == 2
32+
&& arms.iter().all(|arm| arm.guard.is_none())
33+
&& let Some((idx, is_ok)) = arms.iter().enumerate().find_map(|(arm_idx, arm)| {
34+
if let Some((is_ok, ident)) = is_ok_or_err(cx, arm.pat)
35+
&& is_some_ident(cx, arm.body, ident)
36+
{
37+
Some((arm_idx, is_ok))
38+
} else {
39+
None
40+
}
41+
})
42+
// Accept wildcard only as the second arm
43+
&& is_variant_or_wildcard(arms[1-idx].pat, idx == 0)
44+
&& is_none(cx, arms[1 - idx].body)
45+
{
46+
apply_lint(cx, expr, scrutinee, is_ok);
47+
}
48+
}
49+
50+
/// Check that `pat` applied to a `Result` only matches `Ok(_)`, `Err(_)`, not a subset or a
51+
/// superset of it. If `can_be_wild` is `true`, wildcards are also accepted.
52+
fn is_variant_or_wildcard(pat: &Pat<'_>, can_be_wild: bool) -> bool {
53+
match pat.kind {
54+
PatKind::Wild | PatKind::Path(..) if can_be_wild => true,
55+
PatKind::TupleStruct(..) | PatKind::Struct(..) | PatKind::Binding(_, _, _, None) => true,
56+
PatKind::Binding(_, _, _, Some(pat)) | PatKind::Ref(pat, _) => is_variant_or_wildcard(pat, can_be_wild),
57+
_ => false,
58+
}
59+
}
60+
61+
/// Return `Some((true, IDENT))` if `pat` contains `Ok(IDENT)`, `Some((false, IDENT))` if it
62+
/// contains `Err(IDENT)`, `None` otherwise.
63+
fn is_ok_or_err<'hir>(cx: &LateContext<'_>, pat: &Pat<'hir>) -> Option<(bool, &'hir Ident)> {
64+
if let PatKind::TupleStruct(qpath, [arg], _) = &pat.kind
65+
&& let PatKind::Binding(BindingMode::NONE, _, ident, _) = &arg.kind
66+
&& let res = cx.qpath_res(qpath, pat.hir_id)
67+
&& let Res::Def(DefKind::Ctor(..), id) = res
68+
&& let id @ Some(_) = cx.tcx.opt_parent(id)
69+
{
70+
let lang_items = cx.tcx.lang_items();
71+
if id == lang_items.result_ok_variant() {
72+
return Some((true, ident));
73+
} else if id == lang_items.result_err_variant() {
74+
return Some((false, ident));
75+
}
76+
}
77+
None
78+
}
79+
80+
/// Check if `expr` contains `Some(ident)`, possibly as a block
81+
fn is_some_ident(cx: &LateContext<'_>, expr: &Expr<'_>, ident: &Ident) -> bool {
82+
if let ExprKind::Call(body_callee, [body_arg]) = peel_blocks(expr).kind
83+
&& is_res_lang_ctor(cx, path_res(cx, body_callee), OptionSome)
84+
&& let ExprKind::Path(QPath::Resolved(
85+
_,
86+
Path {
87+
segments: [segment], ..
88+
},
89+
)) = body_arg.kind
90+
{
91+
segment.ident.name == ident.name
92+
} else {
93+
false
94+
}
95+
}
96+
97+
/// Check if `expr` is `None`, possibly as a block
98+
fn is_none(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
99+
is_res_lang_ctor(cx, path_res(cx, peel_blocks(expr)), OptionNone)
100+
}
101+
102+
/// Suggest replacing `expr` by `scrutinee.METHOD()`, where `METHOD` is either `ok` or
103+
/// `err`, depending on `is_ok`.
104+
fn apply_lint(cx: &LateContext<'_>, expr: &Expr<'_>, scrutinee: &Expr<'_>, is_ok: bool) {
105+
let method = if is_ok { "ok" } else { "err" };
106+
let mut app = if span_contains_comment(cx.sess().source_map(), expr.span) {
107+
Applicability::MaybeIncorrect
108+
} else {
109+
Applicability::MachineApplicable
110+
};
111+
let scrut = Sugg::hir_with_applicability(cx, scrutinee, "..", &mut app).maybe_par();
112+
span_lint_and_sugg(
113+
cx,
114+
MANUAL_OK_ERR,
115+
expr.span,
116+
format!("manual implementation of `{method}`"),
117+
"replace with",
118+
format!("{scrut}.{method}()"),
119+
app,
120+
);
121+
}

clippy_lints/src/matches/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod collapsible_match;
22
mod infallible_destructuring_match;
33
mod manual_filter;
44
mod manual_map;
5+
mod manual_ok_err;
56
mod manual_unwrap_or;
67
mod manual_utils;
78
mod match_as_ref;
@@ -975,6 +976,40 @@ declare_clippy_lint! {
975976
"checks for unnecessary guards in match expressions"
976977
}
977978

979+
declare_clippy_lint! {
980+
/// ### What it does
981+
/// Checks for manual implementation of `.ok()` or `.err()`
982+
/// on `Result` values.
983+
///
984+
/// ### Why is this bad?
985+
/// Using `.ok()` or `.err()` rather than a `match` or
986+
/// `if let` is less complex and more readable.
987+
///
988+
/// ### Example
989+
/// ```no_run
990+
/// # fn func() -> Result<u32, &'static str> { Ok(0) }
991+
/// let a = match func() {
992+
/// Ok(v) => Some(v),
993+
/// Err(_) => None,
994+
/// };
995+
/// let b = if let Err(v) = func() {
996+
/// Some(v)
997+
/// } else {
998+
/// None
999+
/// };
1000+
/// ```
1001+
/// Use instead:
1002+
/// ```no_run
1003+
/// # fn func() -> Result<u32, &'static str> { Ok(0) }
1004+
/// let a = func().ok();
1005+
/// let b = func().err();
1006+
/// ```
1007+
#[clippy::version = "1.84.0"]
1008+
pub MANUAL_OK_ERR,
1009+
complexity,
1010+
"find manual implementations of `.ok()` or `.err()` on `Result`"
1011+
}
1012+
9781013
pub struct Matches {
9791014
msrv: Msrv,
9801015
infallible_destructuring_match_linted: bool,
@@ -1016,6 +1051,7 @@ impl_lint_pass!(Matches => [
10161051
MANUAL_MAP,
10171052
MANUAL_FILTER,
10181053
REDUNDANT_GUARDS,
1054+
MANUAL_OK_ERR,
10191055
]);
10201056

10211057
impl<'tcx> LateLintPass<'tcx> for Matches {
@@ -1073,6 +1109,7 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
10731109
manual_unwrap_or::check_match(cx, expr, ex, arms);
10741110
manual_map::check_match(cx, expr, ex, arms);
10751111
manual_filter::check_match(cx, ex, arms, expr);
1112+
manual_ok_err::check_match(cx, expr, ex, arms);
10761113
}
10771114

10781115
if self.infallible_destructuring_match_linted {
@@ -1116,6 +1153,14 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
11161153
if_let.if_then,
11171154
else_expr,
11181155
);
1156+
manual_ok_err::check_if_let(
1157+
cx,
1158+
expr,
1159+
if_let.let_pat,
1160+
if_let.let_expr,
1161+
if_let.if_then,
1162+
else_expr,
1163+
);
11191164
}
11201165
}
11211166
redundant_pattern_match::check_if_let(

tests/ui/manual_ok_err.fixed

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#![warn(clippy::manual_ok_err)]
2+
3+
fn funcall() -> Result<u32, &'static str> {
4+
todo!()
5+
}
6+
7+
fn main() {
8+
let _ = funcall().ok();
9+
10+
let _ = funcall().ok();
11+
12+
let _ = funcall().err();
13+
14+
let _ = funcall().err();
15+
16+
let _ = funcall().ok();
17+
18+
let _ = funcall().err();
19+
20+
#[allow(clippy::redundant_pattern)]
21+
let _ = funcall().ok();
22+
23+
struct S;
24+
25+
impl std::ops::Neg for S {
26+
type Output = Result<u32, &'static str>;
27+
28+
fn neg(self) -> Self::Output {
29+
funcall()
30+
}
31+
}
32+
33+
// Suggestion should be properly parenthesized
34+
let _ = (-S).ok();
35+
36+
no_lint();
37+
}
38+
39+
fn no_lint() {
40+
let _ = match funcall() {
41+
Ok(v) if v > 3 => Some(v),
42+
_ => None,
43+
};
44+
45+
let _ = match funcall() {
46+
Err(_) => None,
47+
Ok(3) => None,
48+
Ok(v) => Some(v),
49+
};
50+
51+
let _ = match funcall() {
52+
_ => None,
53+
Ok(v) => Some(v),
54+
};
55+
56+
let _ = match funcall() {
57+
Err(_) | Ok(3) => None,
58+
Ok(v) => Some(v),
59+
};
60+
61+
#[expect(clippy::redundant_pattern)]
62+
let _ = match funcall() {
63+
_v @ _ => None,
64+
Ok(v) => Some(v),
65+
};
66+
}

tests/ui/manual_ok_err.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#![warn(clippy::manual_ok_err)]
2+
3+
fn funcall() -> Result<u32, &'static str> {
4+
todo!()
5+
}
6+
7+
fn main() {
8+
let _ = match funcall() {
9+
//~^ manual_ok_err
10+
Ok(v) => Some(v),
11+
Err(_) => None,
12+
};
13+
14+
let _ = match funcall() {
15+
//~^ manual_ok_err
16+
Ok(v) => Some(v),
17+
_v => None,
18+
};
19+
20+
let _ = match funcall() {
21+
//~^ manual_ok_err
22+
Err(v) => Some(v),
23+
Ok(_) => None,
24+
};
25+
26+
let _ = match funcall() {
27+
//~^ manual_ok_err
28+
Err(v) => Some(v),
29+
_v => None,
30+
};
31+
32+
let _ = if let Ok(v) = funcall() {
33+
//~^ manual_ok_err
34+
Some(v)
35+
} else {
36+
None
37+
};
38+
39+
let _ = if let Err(v) = funcall() {
40+
//~^ manual_ok_err
41+
Some(v)
42+
} else {
43+
None
44+
};
45+
46+
#[allow(clippy::redundant_pattern)]
47+
let _ = match funcall() {
48+
//~^ manual_ok_err
49+
Ok(v) => Some(v),
50+
_v @ _ => None,
51+
};
52+
53+
struct S;
54+
55+
impl std::ops::Neg for S {
56+
type Output = Result<u32, &'static str>;
57+
58+
fn neg(self) -> Self::Output {
59+
funcall()
60+
}
61+
}
62+
63+
// Suggestion should be properly parenthesized
64+
let _ = match -S {
65+
//~^ manual_ok_err
66+
Ok(v) => Some(v),
67+
_ => None,
68+
};
69+
70+
no_lint();
71+
}
72+
73+
fn no_lint() {
74+
let _ = match funcall() {
75+
Ok(v) if v > 3 => Some(v),
76+
_ => None,
77+
};
78+
79+
let _ = match funcall() {
80+
Err(_) => None,
81+
Ok(3) => None,
82+
Ok(v) => Some(v),
83+
};
84+
85+
let _ = match funcall() {
86+
_ => None,
87+
Ok(v) => Some(v),
88+
};
89+
90+
let _ = match funcall() {
91+
Err(_) | Ok(3) => None,
92+
Ok(v) => Some(v),
93+
};
94+
95+
#[expect(clippy::redundant_pattern)]
96+
let _ = match funcall() {
97+
_v @ _ => None,
98+
Ok(v) => Some(v),
99+
};
100+
}

0 commit comments

Comments
 (0)