Skip to content

Commit a5d134c

Browse files
committed
New lint: manual_ok_err
1 parent ff4a26d commit a5d134c

File tree

7 files changed

+389
-0
lines changed

7 files changed

+389
-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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::source::snippet_with_applicability;
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+
&& pat.default_binding_modes
67+
&& let res = cx.qpath_res(qpath, pat.hir_id)
68+
&& let Res::Def(DefKind::Ctor(..), id) = res
69+
&& let id @ Some(_) = cx.tcx.opt_parent(id)
70+
{
71+
let lang_items = cx.tcx.lang_items();
72+
if id == lang_items.result_ok_variant() {
73+
return Some((true, ident));
74+
} else if id == lang_items.result_err_variant() {
75+
return Some((false, ident));
76+
}
77+
}
78+
None
79+
}
80+
81+
/// Check if `expr` contains `Some(ident)`, possibly as a block
82+
fn is_some_ident(cx: &LateContext<'_>, expr: &Expr<'_>, ident: &Ident) -> bool {
83+
if let ExprKind::Call(body_callee, [body_arg]) = peel_blocks(expr).kind
84+
&& is_res_lang_ctor(cx, path_res(cx, body_callee), OptionSome)
85+
&& let ExprKind::Path(QPath::Resolved(
86+
_,
87+
Path {
88+
segments: [segment], ..
89+
},
90+
)) = body_arg.kind
91+
{
92+
segment.ident.name == ident.name
93+
} else {
94+
false
95+
}
96+
}
97+
98+
/// Check if `expr` is `None`, possibly as a block
99+
fn is_none(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
100+
is_res_lang_ctor(cx, path_res(cx, peel_blocks(expr)), OptionNone)
101+
}
102+
103+
/// Suggest replacing `expr` by `scrutinee.METHOD()`, where `METHOD` is either `ok` or
104+
/// `err`, depending on `is_ok`.
105+
fn apply_lint(cx: &LateContext<'_>, expr: &Expr<'_>, scrutinee: &Expr<'_>, is_ok: bool) {
106+
let method = if is_ok { "ok" } else { "err" };
107+
let mut app = if span_contains_comment(cx.sess().source_map(), expr.span) {
108+
Applicability::MaybeIncorrect
109+
} else {
110+
Applicability::MachineApplicable
111+
};
112+
let snippet = snippet_with_applicability(cx.sess(), scrutinee.span, "..", &mut app);
113+
span_lint_and_sugg(
114+
cx,
115+
MANUAL_OK_ERR,
116+
expr.span,
117+
format!("manual implementation of `{method}`"),
118+
"replace with",
119+
format!("{snippet}.{method}()"),
120+
app,
121+
);
122+
}

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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
no_lint();
24+
}
25+
26+
fn no_lint() {
27+
let _ = match funcall() {
28+
Ok(v) if v > 3 => Some(v),
29+
_ => None,
30+
};
31+
32+
let _ = match funcall() {
33+
Err(_) => None,
34+
Ok(3) => None,
35+
Ok(v) => Some(v),
36+
};
37+
38+
let _ = match funcall() {
39+
_ => None,
40+
Ok(v) => Some(v),
41+
};
42+
43+
let _ = match funcall() {
44+
Err(_) | Ok(3) => None,
45+
Ok(v) => Some(v),
46+
};
47+
48+
#[expect(clippy::redundant_pattern)]
49+
let _ = match funcall() {
50+
_v @ _ => None,
51+
Ok(v) => Some(v),
52+
};
53+
}

tests/ui/manual_ok_err.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
no_lint();
54+
}
55+
56+
fn no_lint() {
57+
let _ = match funcall() {
58+
Ok(v) if v > 3 => Some(v),
59+
_ => None,
60+
};
61+
62+
let _ = match funcall() {
63+
Err(_) => None,
64+
Ok(3) => None,
65+
Ok(v) => Some(v),
66+
};
67+
68+
let _ = match funcall() {
69+
_ => None,
70+
Ok(v) => Some(v),
71+
};
72+
73+
let _ = match funcall() {
74+
Err(_) | Ok(3) => None,
75+
Ok(v) => Some(v),
76+
};
77+
78+
#[expect(clippy::redundant_pattern)]
79+
let _ = match funcall() {
80+
_v @ _ => None,
81+
Ok(v) => Some(v),
82+
};
83+
}

0 commit comments

Comments
 (0)