Skip to content

Commit 579571d

Browse files
authored
New lint: manual_ok_err (#13740)
changelog: [`manual_ok_err`]: new lint Detect manual implementations of `.ok()` or `.err()`, as in ```rust let a = match func() { Ok(v) => Some(v), Err(_) => None, }; let b = if let Err(v) = func() { Some(v) } else { None }; ``` which can be replaced by ```rust let a = func().ok(); let b = func().err(); ``` This pattern was detected in the wild in the Rust reimplementation of coreutils: uutils/coreutils#6886 (review)
2 parents 8678f9c + 4a69d0d commit 579571d

File tree

8 files changed

+504
-0
lines changed

8 files changed

+504
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5762,6 +5762,7 @@ Released 2018-09-13
57625762
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
57635763
[`manual_next_back`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_next_back
57645764
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
5765+
[`manual_ok_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_err
57655766
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
57665767
[`manual_pattern_char_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_pattern_char_comparison
57675768
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains

clippy_lints/src/declared_lints.rs

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

clippy_lints/src/matches/mod.rs

+45
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;
@@ -972,6 +973,40 @@ declare_clippy_lint! {
972973
"checks for unnecessary guards in match expressions"
973974
}
974975

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

10181054
impl<'tcx> LateLintPass<'tcx> for Matches {
@@ -1091,6 +1127,7 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
10911127
manual_unwrap_or::check_match(cx, expr, ex, arms);
10921128
manual_map::check_match(cx, expr, ex, arms);
10931129
manual_filter::check_match(cx, ex, arms, expr);
1130+
manual_ok_err::check_match(cx, expr, ex, arms);
10941131
}
10951132

10961133
if self.infallible_destructuring_match_linted {
@@ -1134,6 +1171,14 @@ impl<'tcx> LateLintPass<'tcx> for Matches {
11341171
if_let.if_then,
11351172
else_expr,
11361173
);
1174+
manual_ok_err::check_if_let(
1175+
cx,
1176+
expr,
1177+
if_let.let_pat,
1178+
if_let.let_expr,
1179+
if_let.if_then,
1180+
else_expr,
1181+
);
11371182
}
11381183
}
11391184
redundant_pattern_match::check_if_let(

clippy_utils/src/ty/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -1341,3 +1341,14 @@ pub fn get_field_by_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>, name: Symbol) ->
13411341
_ => None,
13421342
}
13431343
}
1344+
1345+
/// Check if `ty` is an `Option` and return its argument type if it is.
1346+
pub fn option_arg_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
1347+
match ty.kind() {
1348+
ty::Adt(adt, args) => cx
1349+
.tcx
1350+
.is_diagnostic_item(sym::Option, adt.did())
1351+
.then(|| args.type_at(0)),
1352+
_ => None,
1353+
}
1354+
}

tests/ui/manual_ok_err.fixed

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
67+
// Content of `Option` and matching content of `Result` do
68+
// not have the same type.
69+
let _: Option<&dyn std::any::Any> = match Ok::<_, ()>(&1) {
70+
Ok(v) => Some(v),
71+
_ => None,
72+
};
73+
74+
let _ = match Ok::<_, ()>(&1) {
75+
_x => None,
76+
Ok(v) => Some(v),
77+
};
78+
79+
let _ = match Ok::<_, std::convert::Infallible>(1) {
80+
Ok(3) => None,
81+
Ok(v) => Some(v),
82+
};
83+
}
84+
85+
const fn cf(x: Result<u32, &'static str>) -> Option<u32> {
86+
// Do not lint in const code
87+
match x {
88+
Ok(v) => Some(v),
89+
Err(_) => None,
90+
}
91+
}

0 commit comments

Comments
 (0)