Skip to content

Commit e2ccefa

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 3f4c5f5 commit e2ccefa

12 files changed

Lines changed: 301 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6979,6 +6979,7 @@ Released 2018-09-13
69796979
[`if_then_some_else_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_then_some_else_none
69806980
[`ifs_same_cond`]: https://rust-lang.github.io/rust-clippy/master/index.html#ifs_same_cond
69816981
[`ignore_without_reason`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignore_without_reason
6982+
[`ignored_result_err`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignored_result_err
69826983
[`ignored_unit_patterns`]: https://rust-lang.github.io/rust-clippy/master/index.html#ignored_unit_patterns
69836984
[`impl_hash_borrow_with_str_and_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#impl_hash_borrow_with_str_and_bytes
69846985
[`impl_trait_in_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#impl_trait_in_params
@@ -7663,6 +7664,7 @@ Released 2018-09-13
76637664
[`allow-exact-repetitions`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-exact-repetitions
76647665
[`allow-expect-in-consts`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-expect-in-consts
76657666
[`allow-expect-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-expect-in-tests
7667+
[`allow-ignored-result-err-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-ignored-result-err-in-tests
76667668
[`allow-indexing-slicing-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-indexing-slicing-in-tests
76677669
[`allow-large-stack-frames-in-tests`]: https://doc.rust-lang.org/clippy/lint_configuration.html#allow-large-stack-frames-in-tests
76687670
[`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
@@ -371,6 +371,9 @@ define_Conf! {
371371
/// Whether `expect` should be allowed in test functions or `#[cfg(test)]`
372372
#[lints(expect_used)]
373373
allow_expect_in_tests: bool = false,
374+
/// Whether `ignored_result_err` should be allowed in test functions or `#[cfg(test)]`
375+
#[lints(ignored_result_err)]
376+
allow_ignored_result_err_in_tests: bool = false,
374377
/// Whether `indexing_slicing` should be allowed in test functions or `#[cfg(test)]`
375378
#[lints(indexing_slicing)]
376379
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
@@ -214,6 +214,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
214214
crate::ifs::IF_SAME_THEN_ELSE_INFO,
215215
crate::ifs::IFS_SAME_COND_INFO,
216216
crate::ifs::SAME_FUNCTIONS_IN_IF_CONDITION_INFO,
217+
crate::ignored_result_err::IGNORED_RESULT_ERR_INFO,
217218
crate::ignored_unit_patterns::IGNORED_UNIT_PATTERNS_INFO,
218219
crate::impl_hash_with_borrow_str_and_bytes::IMPL_HASH_BORROW_WITH_STR_AND_BYTES_INFO,
219220
crate::implicit_hasher::IMPLICIT_HASHER_INFO,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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::{Expr, LetStmt, Pat, PatKind, QPath};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::impl_lint_pass;
8+
use rustc_span::sym;
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+
///
60+
/// ### Configuration
61+
/// - `allow-ignored-result-err-in-tests`: set to `true` to disable this lint in test code
62+
#[clippy::version = "1.98.0"]
63+
pub IGNORED_RESULT_ERR,
64+
restriction,
65+
"`if let Ok(x) = ...`, `while let Ok(x) = ...`, or `let Ok(x) = ... else` discards the error variant without binding it"
66+
}
67+
68+
impl_lint_pass!(IgnoredResultErr => [IGNORED_RESULT_ERR]);
69+
70+
pub struct IgnoredResultErr {
71+
allow_in_tests: bool,
72+
}
73+
74+
impl IgnoredResultErr {
75+
pub fn new(conf: &'static Conf) -> Self {
76+
Self {
77+
allow_in_tests: conf.allow_ignored_result_err_in_tests,
78+
}
79+
}
80+
}
81+
82+
fn is_ok_pattern_on_result(cx: &LateContext<'_>, pat: &Pat<'_>, scrutinee: &Expr<'_>) -> bool {
83+
cx.typeck_results().expr_ty(scrutinee).is_diag_item(cx, sym::Result)
84+
&& matches!(
85+
pat.kind,
86+
PatKind::TupleStruct(QPath::Resolved(_, path), _, _)
87+
if path.segments.last().is_some_and(|s| s.ident.name == sym::Ok)
88+
)
89+
}
90+
91+
impl<'tcx> LateLintPass<'tcx> for IgnoredResultErr {
92+
fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx LetStmt<'_>) {
93+
if self.allow_in_tests && is_in_test(cx.tcx, local.hir_id) {
94+
return;
95+
}
96+
97+
if local.els.is_some()
98+
&& let Some(init) = local.init
99+
&& is_ok_pattern_on_result(cx, local.pat, init)
100+
{
101+
span_lint_and_help(
102+
cx,
103+
IGNORED_RESULT_ERR,
104+
local.span,
105+
"this `let Ok(...) = ... else` discards the `Err` variant",
106+
None,
107+
"consider using `match` and binding the `Err` value for logging or recovery",
108+
);
109+
}
110+
}
111+
112+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
113+
if self.allow_in_tests && is_in_test(cx.tcx, expr.hir_id) {
114+
return;
115+
}
116+
117+
if let Some(if_let) = higher::IfLet::hir(cx, expr)
118+
&& is_ok_pattern_on_result(cx, if_let.let_pat, if_let.let_expr)
119+
{
120+
span_lint_and_help(
121+
cx,
122+
IGNORED_RESULT_ERR,
123+
expr.span,
124+
"this `if let Ok(...)` discards the `Err` variant",
125+
None,
126+
"consider using `match` and binding the `Err` value for logging or recovery",
127+
);
128+
} else if let Some(while_let) = higher::WhileLet::hir(expr)
129+
&& is_ok_pattern_on_result(cx, while_let.let_pat, while_let.let_expr)
130+
{
131+
span_lint_and_help(
132+
cx,
133+
IGNORED_RESULT_ERR,
134+
expr.span,
135+
"this `while let Ok(...)` discards the `Err` variant",
136+
None,
137+
"consider using `loop` + `match` and binding the `Err` value for logging or recovery",
138+
);
139+
}
140+
}
141+
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ mod if_let_mutex;
156156
mod if_not_else;
157157
mod if_then_some_else_none;
158158
mod ifs;
159+
mod ignored_result_err;
159160
mod ignored_unit_patterns;
160161
mod impl_hash_with_borrow_str_and_bytes;
161162
mod implicit_hasher;
@@ -868,6 +869,7 @@ rustc_lint::late_lint_methods!(
868869
RedundantElse: redundant_else::RedundantElse = redundant_else::RedundantElse,
869870
RestWhenDestructuringStruct: rest_when_destructuring_struct::RestWhenDestructuringStruct = rest_when_destructuring_struct::RestWhenDestructuringStruct,
870871
BlockScrutinee: block_scrutinee::BlockScrutinee = block_scrutinee::BlockScrutinee,
872+
IgnoredResultErr: ignored_result_err::IgnoredResultErr = ignored_result_err::IgnoredResultErr::new(conf),
871873
// add late passes here, used by `cargo dev new_lint`
872874
]]
873875
);
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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
88
allow-exact-repetitions
99
allow-expect-in-consts
1010
allow-expect-in-tests
11+
allow-ignored-result-err-in-tests
1112
allow-indexing-slicing-in-tests
1213
allow-large-stack-frames-in-tests
1314
allow-mixed-uninlined-format-args
@@ -111,6 +112,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
111112
allow-exact-repetitions
112113
allow-expect-in-consts
113114
allow-expect-in-tests
115+
allow-ignored-result-err-in-tests
114116
allow-indexing-slicing-in-tests
115117
allow-large-stack-frames-in-tests
116118
allow-mixed-uninlined-format-args
@@ -214,6 +216,7 @@ error: error reading Clippy's configuration file: unknown field `allow_mixed_uni
214216
allow-exact-repetitions
215217
allow-expect-in-consts
216218
allow-expect-in-tests
219+
allow-ignored-result-err-in-tests
217220
allow-indexing-slicing-in-tests
218221
allow-large-stack-frames-in-tests
219222
allow-mixed-uninlined-format-args

0 commit comments

Comments
 (0)