Skip to content

Commit 64d08a8

Browse files
committed
Auto merge of #12005 - PartiallyTyped:11713, r=blyxyas
`unused_io_amount` captures `Ok(_)`s Partial rewrite of `unused_io_amount` to lint over `Ok(_)` and `Ok(..)`. Moved the check to `check_block` to simplify context checking for expressions and allow us to check only some expressions. For match (expr, arms) we emit a lint for io ops used on `expr` when an arm is `Ok(_)|Ok(..)`. Also considers the cases when there are guards in the arms and `if let Ok(_) = ...` cases. For `Ok(_)` and `Ok(..)` it emits a note indicating where the value is ignored. changelog: False Negatives [`unused_io_amount`]: Extended `unused_io_amount` to catch `Ok(_)`s in `If let` and match exprs. Closes #11713 r? `@giraffate`
2 parents 7386856 + f73879e commit 64d08a8

File tree

3 files changed

+400
-120
lines changed

3 files changed

+400
-120
lines changed

clippy_lints/src/unused_io_amount.rs

+193-99
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
2-
use clippy_utils::{is_trait_method, is_try, match_trait_method, paths};
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::{is_res_lang_ctor, is_trait_method, match_trait_method, paths};
3+
use hir::{ExprKind, PatKind};
34
use rustc_hir as hir;
45
use rustc_lint::{LateContext, LateLintPass};
56
use rustc_session::declare_lint_pass;
6-
use rustc_span::sym;
7+
use rustc_span::{sym, Span};
78

89
declare_clippy_lint! {
910
/// ### What it does
@@ -45,126 +46,219 @@ declare_clippy_lint! {
4546

4647
declare_lint_pass!(UnusedIoAmount => [UNUSED_IO_AMOUNT]);
4748

49+
#[derive(Copy, Clone)]
50+
enum IoOp {
51+
AsyncWrite(bool),
52+
AsyncRead(bool),
53+
SyncRead(bool),
54+
SyncWrite(bool),
55+
}
56+
4857
impl<'tcx> LateLintPass<'tcx> for UnusedIoAmount {
49-
fn check_stmt(&mut self, cx: &LateContext<'_>, s: &hir::Stmt<'_>) {
50-
let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = s.kind else {
51-
return;
52-
};
58+
/// We perform the check on the block level.
59+
/// If we want to catch match and if expressions that act as returns of the block
60+
/// we need to check them at `check_expr` or `check_block` as they are not stmts
61+
/// but we can't check them at `check_expr` because we need the broader context
62+
/// because we should do this only for the final expression of the block, and not for
63+
/// `StmtKind::Local` which binds values => the io amount is used.
64+
///
65+
/// To check for unused io amount in stmts, we only consider `StmtKind::Semi`.
66+
/// `StmtKind::Local` is not considered because it binds values => the io amount is used.
67+
/// `StmtKind::Expr` is not considered because requires unit type => the io amount is used.
68+
/// `StmtKind::Item` is not considered because it's not an expression.
69+
///
70+
/// We then check the individual expressions via `check_expr`. We use the same logic for
71+
/// semi expressions and the final expression as we need to check match and if expressions
72+
/// for binding of the io amount to `Ok(_)`.
73+
///
74+
/// We explicitly check for the match source to be Normal as it needs special logic
75+
/// to consider the arms, and we want to avoid breaking the logic for situations where things
76+
/// get desugared to match.
77+
fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'tcx>) {
78+
for stmt in block.stmts {
79+
if let hir::StmtKind::Semi(exp) = stmt.kind {
80+
check_expr(cx, exp);
81+
}
82+
}
5383

54-
match expr.kind {
55-
hir::ExprKind::Match(res, _, _) if is_try(cx, expr).is_some() => {
56-
if let hir::ExprKind::Call(func, [ref arg_0, ..]) = res.kind {
57-
if matches!(
58-
func.kind,
59-
hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::TryTraitBranch, ..))
60-
) {
61-
check_map_error(cx, arg_0, expr);
84+
if let Some(exp) = block.expr
85+
&& matches!(exp.kind, hir::ExprKind::If(_, _, _) | hir::ExprKind::Match(_, _, _))
86+
{
87+
check_expr(cx, exp);
88+
}
89+
}
90+
}
91+
92+
fn check_expr<'a>(cx: &LateContext<'a>, expr: &'a hir::Expr<'a>) {
93+
match expr.kind {
94+
hir::ExprKind::If(cond, _, _)
95+
if let ExprKind::Let(hir::Let { pat, init, .. }) = cond.kind
96+
&& pattern_is_ignored_ok(cx, pat)
97+
&& let Some(op) = should_lint(cx, init) =>
98+
{
99+
emit_lint(cx, cond.span, op, &[pat.span]);
100+
},
101+
hir::ExprKind::Match(expr, arms, hir::MatchSource::Normal) if let Some(op) = should_lint(cx, expr) => {
102+
let found_arms: Vec<_> = arms
103+
.iter()
104+
.filter_map(|arm| {
105+
if pattern_is_ignored_ok(cx, arm.pat) {
106+
Some(arm.span)
107+
} else {
108+
None
62109
}
63-
} else {
64-
check_map_error(cx, res, expr);
65-
}
66-
},
67-
hir::ExprKind::MethodCall(path, arg_0, ..) => match path.ident.as_str() {
68-
"expect" | "unwrap" | "unwrap_or" | "unwrap_or_else" | "is_ok" | "is_err" => {
69-
check_map_error(cx, arg_0, expr);
70-
},
71-
_ => (),
72-
},
73-
_ => (),
110+
})
111+
.collect();
112+
if !found_arms.is_empty() {
113+
emit_lint(cx, expr.span, op, found_arms.as_slice());
114+
}
115+
},
116+
_ if let Some(op) = should_lint(cx, expr) => {
117+
emit_lint(cx, expr.span, op, &[]);
118+
},
119+
_ => {},
120+
};
121+
}
122+
123+
fn should_lint<'a>(cx: &LateContext<'a>, mut inner: &'a hir::Expr<'a>) -> Option<IoOp> {
124+
inner = unpack_match(inner);
125+
inner = unpack_try(inner);
126+
inner = unpack_call_chain(inner);
127+
inner = unpack_await(inner);
128+
// we type-check it to get whether it's a read/write or their vectorized forms
129+
// and keep only the ones that are produce io amount
130+
check_io_mode(cx, inner)
131+
}
132+
133+
fn pattern_is_ignored_ok(cx: &LateContext<'_>, pat: &hir::Pat<'_>) -> bool {
134+
// the if checks whether we are in a result Ok( ) pattern
135+
// and the return checks whether it is unhandled
136+
137+
if let PatKind::TupleStruct(ref path, inner_pat, ddp) = pat.kind
138+
// we check against Result::Ok to avoid linting on Err(_) or something else.
139+
&& is_res_lang_ctor(cx, cx.qpath_res(path, pat.hir_id), hir::LangItem::ResultOk)
140+
{
141+
return match (inner_pat, ddp.as_opt_usize()) {
142+
// Ok(_) pattern
143+
([inner_pat], None) if matches!(inner_pat.kind, PatKind::Wild) => true,
144+
// Ok(..) pattern
145+
([], Some(0)) => true,
146+
_ => false,
147+
};
148+
}
149+
false
150+
}
151+
152+
fn unpack_call_chain<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> {
153+
while let hir::ExprKind::MethodCall(path, receiver, ..) = expr.kind {
154+
if matches!(
155+
path.ident.as_str(),
156+
"unwrap" | "expect" | "unwrap_or" | "unwrap_or_else" | "ok" | "is_ok" | "is_err" | "or_else" | "or"
157+
) {
158+
expr = receiver;
159+
} else {
160+
break;
74161
}
75162
}
163+
expr
164+
}
165+
166+
fn unpack_try<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> {
167+
while let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind
168+
&& matches!(
169+
func.kind,
170+
hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::TryTraitBranch, ..))
171+
)
172+
{
173+
expr = arg_0;
174+
}
175+
expr
176+
}
177+
178+
fn unpack_match<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> {
179+
while let hir::ExprKind::Match(res, _, _) = expr.kind {
180+
expr = res;
181+
}
182+
expr
76183
}
77184

78185
/// If `expr` is an (e).await, return the inner expression "e" that's being
79186
/// waited on. Otherwise return None.
80-
fn try_remove_await<'a>(expr: &'a hir::Expr<'a>) -> Option<&hir::Expr<'a>> {
187+
fn unpack_await<'a>(expr: &'a hir::Expr<'a>) -> &hir::Expr<'a> {
81188
if let hir::ExprKind::Match(expr, _, hir::MatchSource::AwaitDesugar) = expr.kind {
82189
if let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind {
83190
if matches!(
84191
func.kind,
85192
hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::IntoFutureIntoFuture, ..))
86193
) {
87-
return Some(arg_0);
194+
return arg_0;
88195
}
89196
}
90197
}
91-
92-
None
198+
expr
93199
}
94200

95-
fn check_map_error(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>) {
96-
let mut call = call;
97-
while let hir::ExprKind::MethodCall(path, receiver, ..) = call.kind {
98-
if matches!(path.ident.as_str(), "or" | "or_else" | "ok") {
99-
call = receiver;
100-
} else {
101-
break;
102-
}
103-
}
201+
/// Check whether the current expr is a function call for an IO operation
202+
fn check_io_mode(cx: &LateContext<'_>, call: &hir::Expr<'_>) -> Option<IoOp> {
203+
let hir::ExprKind::MethodCall(path, ..) = call.kind else {
204+
return None;
205+
};
104206

105-
if let Some(call) = try_remove_await(call) {
106-
check_method_call(cx, call, expr, true);
107-
} else {
108-
check_method_call(cx, call, expr, false);
207+
let vectorized = match path.ident.as_str() {
208+
"write_vectored" | "read_vectored" => true,
209+
"write" | "read" => false,
210+
_ => {
211+
return None;
212+
},
213+
};
214+
215+
match (
216+
is_trait_method(cx, call, sym::IoRead),
217+
is_trait_method(cx, call, sym::IoWrite),
218+
match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCREADEXT)
219+
|| match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCREADEXT),
220+
match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCWRITEEXT)
221+
|| match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCWRITEEXT),
222+
) {
223+
(true, _, _, _) => Some(IoOp::SyncRead(vectorized)),
224+
(_, true, _, _) => Some(IoOp::SyncWrite(vectorized)),
225+
(_, _, true, _) => Some(IoOp::AsyncRead(vectorized)),
226+
(_, _, _, true) => Some(IoOp::AsyncWrite(vectorized)),
227+
_ => None,
109228
}
110229
}
111230

112-
fn check_method_call(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>, is_await: bool) {
113-
if let hir::ExprKind::MethodCall(path, ..) = call.kind {
114-
let symbol = path.ident.as_str();
115-
let read_trait = if is_await {
116-
match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCREADEXT)
117-
|| match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCREADEXT)
118-
} else {
119-
is_trait_method(cx, call, sym::IoRead)
120-
};
121-
let write_trait = if is_await {
122-
match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCWRITEEXT)
123-
|| match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCWRITEEXT)
124-
} else {
125-
is_trait_method(cx, call, sym::IoWrite)
126-
};
231+
fn emit_lint(cx: &LateContext<'_>, span: Span, op: IoOp, wild_cards: &[Span]) {
232+
let (msg, help) = match op {
233+
IoOp::AsyncRead(false) => (
234+
"read amount is not handled",
235+
Some("use `AsyncReadExt::read_exact` instead, or handle partial reads"),
236+
),
237+
IoOp::SyncRead(false) => (
238+
"read amount is not handled",
239+
Some("use `Read::read_exact` instead, or handle partial reads"),
240+
),
241+
IoOp::SyncWrite(false) => (
242+
"written amount is not handled",
243+
Some("use `Write::write_all` instead, or handle partial writes"),
244+
),
245+
IoOp::AsyncWrite(false) => (
246+
"written amount is not handled",
247+
Some("use `AsyncWriteExt::write_all` instead, or handle partial writes"),
248+
),
249+
IoOp::SyncRead(true) | IoOp::AsyncRead(true) => ("read amount is not handled", None),
250+
IoOp::SyncWrite(true) | IoOp::AsyncWrite(true) => ("written amount is not handled", None),
251+
};
127252

128-
match (read_trait, write_trait, symbol, is_await) {
129-
(true, _, "read", false) => span_lint_and_help(
130-
cx,
131-
UNUSED_IO_AMOUNT,
132-
expr.span,
133-
"read amount is not handled",
134-
None,
135-
"use `Read::read_exact` instead, or handle partial reads",
136-
),
137-
(true, _, "read", true) => span_lint_and_help(
138-
cx,
139-
UNUSED_IO_AMOUNT,
140-
expr.span,
141-
"read amount is not handled",
142-
None,
143-
"use `AsyncReadExt::read_exact` instead, or handle partial reads",
144-
),
145-
(true, _, "read_vectored", _) => {
146-
span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "read amount is not handled");
147-
},
148-
(_, true, "write", false) => span_lint_and_help(
149-
cx,
150-
UNUSED_IO_AMOUNT,
151-
expr.span,
152-
"written amount is not handled",
153-
None,
154-
"use `Write::write_all` instead, or handle partial writes",
155-
),
156-
(_, true, "write", true) => span_lint_and_help(
157-
cx,
158-
UNUSED_IO_AMOUNT,
159-
expr.span,
160-
"written amount is not handled",
161-
None,
162-
"use `AsyncWriteExt::write_all` instead, or handle partial writes",
163-
),
164-
(_, true, "write_vectored", _) => {
165-
span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "written amount is not handled");
166-
},
167-
_ => (),
253+
span_lint_and_then(cx, UNUSED_IO_AMOUNT, span, msg, |diag| {
254+
if let Some(help_str) = help {
255+
diag.help(help_str);
168256
}
169-
}
257+
for span in wild_cards {
258+
diag.span_note(
259+
*span,
260+
"the result is consumed here, but the amount of I/O bytes remains unhandled",
261+
);
262+
}
263+
});
170264
}

0 commit comments

Comments
 (0)