Skip to content

Commit df410d8

Browse files
author
Daine Mamacos
committed
Add ignored_result_err lint to detect discarded Result error variants
Add a new restriction lint that warns when the Err variant of a Result is implicitly discarded. The lint detects three patterns: - `if let Ok(x) = expr` (with or without else) - `while let Ok(x) = expr` - `let Ok(x) = expr else { ... }` In all these cases, the error value is lost, preventing detailed logging and making error recovery impossible. The lint suggests using `match` with an explicit `Err(e)` binding instead. This is an opt-in restriction lint, enabled with: #![warn(clippy::ignored_result_err)] A configuration option `allow-ignored-result-err-in-tests = true` can be set in clippy.toml to suppress the lint in `#[test]` functions and `#[cfg(test)]` modules. Tested via the compile-test UI test harness with cases covering all three patterns, a negative case for `match` with bound Err, and a ui-toml test verifying the allow-in-tests configuration works correctly.
1 parent b8f6009 commit df410d8

12 files changed

Lines changed: 299 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6985,6 +6985,7 @@ Released 2018-09-13
69856985
[`if_then_some_else_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_then_some_else_none
69866986
[`ifs_same_cond`]: https://rust-lang.github.io/rust-clippy/master/index.html#ifs_same_cond
69876987
[`ignore_without_reason`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignore_without_reason
6988+
[`ignored_result_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignored_result_err
69886989
[`ignored_unit_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignored_unit_patterns
69896990
[`impl_hash_borrow_with_str_and_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#impl_hash_borrow_with_str_and_bytes
69906991
[`impl_trait_in_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#impl_trait_in_params
@@ -7670,6 +7671,7 @@ Released 2018-09-13
76707671
[`allow-exact-repetitions`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-exact-repetitions
76717672
[`allow-expect-in-consts`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-expect-in-consts
76727673
[`allow-expect-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-expect-in-tests
7674+
[`allow-ignored-result-err-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-ignored-result-err-in-tests
76737675
[`allow-indexing-slicing-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-indexing-slicing-in-tests
76747676
[`allow-large-stack-frames-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-large-stack-frames-in-tests
76757677
[`allow-mixed-uninlined-format-args`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-mixed-uninlined-format-args

book/src/lint_configuration.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ Whether `expect` should be allowed in test functions or `#[cfg(test)]`
101101
* [`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#expect_used)
102102

103103

104+
## `allow-ignored-result-err-in-tests`
105+
Whether `ignored_result_err` should be allowed in test functions or `#[cfg(test)]`
106+
107+
**Default Value:** `false`
108+
109+
---
110+
**Affected lints:**
111+
* [`ignored_result_err`](https://rust-lang.github.io/rust-clippy/master/index.html#ignored_result_err)
112+
113+
104114
## `allow-indexing-slicing-in-tests`
105115
Whether `indexing_slicing` should be allowed in test functions or `#[cfg(test)]`
106116

clippy_config/src/conf.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ define_Conf! {
247247
/// Whether `expect` should be allowed in test functions or `#[cfg(test)]`
248248
#[lints(expect_used)]
249249
allow_expect_in_tests("allow-expect-in-tests"): bool = false,
250+
/// Whether `ignored_result_err` should be allowed in test functions or `#[cfg(test)]`
251+
#[lints(ignored_result_err)]
252+
allow_ignored_result_err_in_tests("allow-ignored-result-err-in-tests"): bool = false,
250253
/// Whether `indexing_slicing` should be allowed in test functions or `#[cfg(test)]`
251254
#[lints(indexing_slicing)]
252255
allow_indexing_slicing_in_tests("allow-indexing-slicing-in-tests"): bool = false,

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
215215
crate::ifs::IF_SAME_THEN_ELSE_INFO,
216216
crate::ifs::IFS_SAME_COND_INFO,
217217
crate::ifs::SAME_FUNCTIONS_IN_IF_CONDITION_INFO,
218+
crate::ignored_result_err::IGNORED_RESULT_ERR_INFO,
218219
crate::ignored_unit_patterns::IGNORED_UNIT_PATTERNS_INFO,
219220
crate::impl_hash_with_borrow_str_and_bytes::IMPL_HASH_BORROW_WITH_STR_AND_BYTES_INFO,
220221
crate::implicit_hasher::IMPLICIT_HASHER_INFO,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use clippy_config::Conf;
2+
use clippy_utils::diagnostics::span_lint_and_help;
3+
use clippy_utils::res::MaybeDef as _;
4+
use clippy_utils::{higher, is_in_test};
5+
use rustc_hir::LangItem::ResultOk;
6+
use rustc_hir::{Expr, LetStmt, Pat, PatKind};
7+
use rustc_lint::{LateContext, LateLintPass};
8+
use rustc_session::impl_lint_pass;
9+
10+
declare_clippy_lint! {
11+
/// ### What it does
12+
/// Checks for `if let Ok(x) = expr`, `while let Ok(x) = expr`, and
13+
/// `let Ok(x) = expr else { ... }` where the `Err` variant is discarded
14+
/// without binding.
15+
///
16+
/// ### Why is this bad?
17+
/// The error value contains context about what went wrong. Discarding it
18+
/// prevents detailed logging and makes error recovery impossible.
19+
///
20+
/// ### Example
21+
/// ```rust,ignore
22+
/// if let Ok(res) = some_call() {
23+
/// use_res(res);
24+
/// } else {
25+
/// error!("Something went wrong");
26+
/// }
27+
///
28+
/// while let Ok(line) = reader.read_line() {
29+
/// process(line);
30+
/// }
31+
///
32+
/// let Ok(val) = some_call() else { return; };
33+
/// ```
34+
/// Use instead:
35+
/// ```rust,ignore
36+
/// match some_call() {
37+
/// Ok(res) => use_res(res),
38+
/// Err(e) => error!("Something went wrong: {}", e),
39+
/// }
40+
///
41+
/// loop {
42+
/// match reader.read_line() {
43+
/// Ok(line) => process(line),
44+
/// Err(e) => {
45+
/// error!("Read failed: {}", e);
46+
/// break;
47+
/// }
48+
/// }
49+
/// }
50+
///
51+
/// match some_call() {
52+
/// Ok(val) => { /* use val */ },
53+
/// Err(e) => {
54+
/// error!("Failed: {}", e);
55+
/// return;
56+
/// }
57+
/// }
58+
/// ```
59+
#[clippy::version = "1.98.0"]
60+
pub IGNORED_RESULT_ERR,
61+
restriction,
62+
"`if let Ok(x) = ...`, `while let Ok(x) = ...`, or `let Ok(x) = ... else` discards the error variant without binding it"
63+
}
64+
65+
impl_lint_pass!(IgnoredResultErr => [IGNORED_RESULT_ERR]);
66+
67+
pub struct IgnoredResultErr {
68+
allow_in_tests: bool,
69+
}
70+
71+
impl IgnoredResultErr {
72+
pub fn new(conf: &'static Conf) -> Self {
73+
Self {
74+
allow_in_tests: conf.allow_ignored_result_err_in_tests,
75+
}
76+
}
77+
}
78+
79+
fn is_ok_pattern(cx: &LateContext<'_>, pat: &Pat<'_>) -> bool {
80+
if let PatKind::TupleStruct(ref qpath, ..) = pat.kind {
81+
cx.qpath_res(qpath, pat.hir_id)
82+
.ctor_parent(cx)
83+
.is_lang_item(cx, ResultOk)
84+
} else {
85+
false
86+
}
87+
}
88+
89+
impl<'tcx> LateLintPass<'tcx> for IgnoredResultErr {
90+
fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx LetStmt<'_>) {
91+
if self.allow_in_tests && is_in_test(cx.tcx, local.hir_id) {
92+
return;
93+
}
94+
95+
if local.els.is_some() && is_ok_pattern(cx, local.pat) {
96+
span_lint_and_help(
97+
cx,
98+
IGNORED_RESULT_ERR,
99+
local.span,
100+
"this `let Ok(...) = ... else` discards the `Err` variant",
101+
None,
102+
"consider using `match` and binding the `Err` value for logging or recovery",
103+
);
104+
}
105+
}
106+
107+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
108+
if self.allow_in_tests && is_in_test(cx.tcx, expr.hir_id) {
109+
return;
110+
}
111+
112+
if let Some(if_let) = higher::IfLet::hir(cx, expr)
113+
&& is_ok_pattern(cx, if_let.let_pat)
114+
{
115+
span_lint_and_help(
116+
cx,
117+
IGNORED_RESULT_ERR,
118+
expr.span,
119+
"this `if let Ok(...)` discards the `Err` variant",
120+
None,
121+
"consider using `match` and binding the `Err` value for logging or recovery",
122+
);
123+
} else if let Some(while_let) = higher::WhileLet::hir(expr)
124+
&& is_ok_pattern(cx, while_let.let_pat)
125+
{
126+
span_lint_and_help(
127+
cx,
128+
IGNORED_RESULT_ERR,
129+
expr.span,
130+
"this `while let Ok(...)` discards the `Err` variant",
131+
None,
132+
"consider using `loop` + `match` and binding the `Err` value for logging or recovery",
133+
);
134+
}
135+
}
136+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ mod if_let_mutex;
157157
mod if_not_else;
158158
mod if_then_some_else_none;
159159
mod ifs;
160+
mod ignored_result_err;
160161
mod ignored_unit_patterns;
161162
mod impl_hash_with_borrow_str_and_bytes;
162163
mod implicit_hasher;
@@ -871,6 +872,7 @@ rustc_lint::late_lint_methods!(
871872
RestWhenDestructuringStruct: rest_when_destructuring_struct::RestWhenDestructuringStruct = rest_when_destructuring_struct::RestWhenDestructuringStruct,
872873
BlockScrutinee: block_scrutinee::BlockScrutinee = block_scrutinee::BlockScrutinee,
873874
NonnullUncheckedOnBoxPtr: nonnull_unchecked_on_box_ptr::NonnullUncheckedOnBoxPtr = nonnull_unchecked_on_box_ptr::NonnullUncheckedOnBoxPtr::new(conf),
875+
IgnoredResultErr: ignored_result_err::IgnoredResultErr = ignored_result_err::IgnoredResultErr::new(conf),
874876
// add late passes here, used by `cargo dev new_lint`
875877
]]
876878
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
allow-ignored-result-err-in-tests = true
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#![warn(clippy::ignored_result_err)]
2+
3+
fn some_call() -> Result<i32, String> {
4+
Ok(42)
5+
}
6+
7+
// Should lint — not in test context
8+
fn main() {
9+
if let Ok(res) = some_call() {
10+
//~^ ignored_result_err
11+
println!("{res}");
12+
}
13+
}
14+
15+
// Should NOT lint — in a #[test] function (allow-ignored-result-err-in-tests = true)
16+
#[test]
17+
fn test_something() {
18+
if let Ok(res) = some_call() {
19+
println!("{res}");
20+
}
21+
}
22+
23+
// Should NOT lint — in a #[cfg(test)] module
24+
#[cfg(test)]
25+
mod tests {
26+
use super::some_call;
27+
28+
fn helper() {
29+
if let Ok(res) = some_call() {
30+
println!("{res}");
31+
}
32+
}
33+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: this `if let Ok(...)` discards the `Err` variant
2+
--> tests/ui-toml/ignored_result_err/ignored_result_err.rs:9:5
3+
|
4+
LL | / if let Ok(res) = some_call() {
5+
LL | |
6+
LL | | println!("{res}");
7+
LL | | }
8+
| |_____^
9+
|
10+
= help: consider using `match` and binding the `Err` value for logging or recovery
11+
= note: `-D clippy::ignored-result-err` implied by `-D warnings`
12+
= help: to override `-D warnings` add `#[allow(clippy::ignored_result_err)]`
13+
14+
error: aborting due to 1 previous error
15+

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ LL | foobar = 42
1414
allow-exact-repetitions
1515
allow-expect-in-consts
1616
allow-expect-in-tests
17+
allow-ignored-result-err-in-tests
1718
allow-indexing-slicing-in-tests
1819
allow-large-stack-frames-in-tests
1920
allow-mixed-uninlined-format-args

0 commit comments

Comments
 (0)