use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::{is_res_lang_ctor, is_trait_method, match_trait_method, paths}; use hir::{ExprKind, PatKind}; use rustc_hir as hir; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::{sym, Span}; declare_clippy_lint! { /// ### What it does /// Checks for unused written/read amount. /// /// ### Why is this bad? /// `io::Write::write(_vectored)` and /// `io::Read::read(_vectored)` are not guaranteed to /// process the entire buffer. They return how many bytes were processed, which /// might be smaller /// than a given buffer's length. If you don't need to deal with /// partial-write/read, use /// `write_all`/`read_exact` instead. /// /// When working with asynchronous code (either with the `futures` /// crate or with `tokio`), a similar issue exists for /// `AsyncWriteExt::write()` and `AsyncReadExt::read()` : these /// functions are also not guaranteed to process the entire /// buffer. Your code should either handle partial-writes/reads, or /// call the `write_all`/`read_exact` methods on those traits instead. /// /// ### Known problems /// Detects only common patterns. /// /// ### Examples /// ```rust,ignore /// use std::io; /// fn foo(w: &mut W) -> io::Result<()> { /// // must be `w.write_all(b"foo")?;` /// w.write(b"foo")?; /// Ok(()) /// } /// ``` #[clippy::version = "pre 1.29.0"] pub UNUSED_IO_AMOUNT, correctness, "unused written/read amount" } declare_lint_pass!(UnusedIoAmount => [UNUSED_IO_AMOUNT]); #[derive(Copy, Clone)] enum IoOp { AsyncWrite(bool), AsyncRead(bool), SyncRead(bool), SyncWrite(bool), } impl<'tcx> LateLintPass<'tcx> for UnusedIoAmount { /// We perform the check on the block level. /// If we want to catch match and if expressions that act as returns of the block /// we need to check them at `check_expr` or `check_block` as they are not stmts /// but we can't check them at `check_expr` because we need the broader context /// because we should do this only for the final expression of the block, and not for /// `StmtKind::Local` which binds values => the io amount is used. /// /// To check for unused io amount in stmts, we only consider `StmtKind::Semi`. /// `StmtKind::Local` is not considered because it binds values => the io amount is used. /// `StmtKind::Expr` is not considered because requires unit type => the io amount is used. /// `StmtKind::Item` is not considered because it's not an expression. /// /// We then check the individual expressions via `check_expr`. We use the same logic for /// semi expressions and the final expression as we need to check match and if expressions /// for binding of the io amount to `Ok(_)`. /// /// We explicitly check for the match source to be Normal as it needs special logic /// to consider the arms, and we want to avoid breaking the logic for situations where things /// get desugared to match. fn check_block(&mut self, cx: &LateContext<'tcx>, block: &'tcx hir::Block<'tcx>) { for stmt in block.stmts { if let hir::StmtKind::Semi(exp) = stmt.kind { check_expr(cx, exp); } } if let Some(exp) = block.expr && matches!(exp.kind, hir::ExprKind::If(_, _, _) | hir::ExprKind::Match(_, _, _)) { check_expr(cx, exp); } } } fn check_expr<'a>(cx: &LateContext<'a>, expr: &'a hir::Expr<'a>) { match expr.kind { hir::ExprKind::If(cond, _, _) if let ExprKind::Let(hir::Let { pat, init, .. }) = cond.kind && pattern_is_ignored_ok(cx, pat) && let Some(op) = should_lint(cx, init) => { emit_lint(cx, cond.span, op, &[pat.span]); }, hir::ExprKind::Match(expr, arms, hir::MatchSource::Normal) if let Some(op) = should_lint(cx, expr) => { let found_arms: Vec<_> = arms .iter() .filter_map(|arm| { if pattern_is_ignored_ok(cx, arm.pat) { Some(arm.span) } else { None } }) .collect(); if !found_arms.is_empty() { emit_lint(cx, expr.span, op, found_arms.as_slice()); } }, _ if let Some(op) = should_lint(cx, expr) => { emit_lint(cx, expr.span, op, &[]); }, _ => {}, }; } fn should_lint<'a>(cx: &LateContext<'a>, mut inner: &'a hir::Expr<'a>) -> Option { inner = unpack_match(inner); inner = unpack_try(inner); inner = unpack_call_chain(inner); inner = unpack_await(inner); // we type-check it to get whether it's a read/write or their vectorized forms // and keep only the ones that are produce io amount check_io_mode(cx, inner) } fn pattern_is_ignored_ok(cx: &LateContext<'_>, pat: &hir::Pat<'_>) -> bool { // the if checks whether we are in a result Ok( ) pattern // and the return checks whether it is unhandled if let PatKind::TupleStruct(ref path, inner_pat, ddp) = pat.kind // we check against Result::Ok to avoid linting on Err(_) or something else. && is_res_lang_ctor(cx, cx.qpath_res(path, pat.hir_id), hir::LangItem::ResultOk) { return match (inner_pat, ddp.as_opt_usize()) { // Ok(_) pattern ([inner_pat], None) if matches!(inner_pat.kind, PatKind::Wild) => true, // Ok(..) pattern ([], Some(0)) => true, _ => false, }; } false } fn unpack_call_chain<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> { while let hir::ExprKind::MethodCall(path, receiver, ..) = expr.kind { if matches!( path.ident.as_str(), "unwrap" | "expect" | "unwrap_or" | "unwrap_or_else" | "ok" | "is_ok" | "is_err" | "or_else" | "or" ) { expr = receiver; } else { break; } } expr } fn unpack_try<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> { while let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind && matches!( func.kind, hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::TryTraitBranch, ..)) ) { expr = arg_0; } expr } fn unpack_match<'a>(mut expr: &'a hir::Expr<'a>) -> &'a hir::Expr<'a> { while let hir::ExprKind::Match(res, _, _) = expr.kind { expr = res; } expr } /// If `expr` is an (e).await, return the inner expression "e" that's being /// waited on. Otherwise return None. fn unpack_await<'a>(expr: &'a hir::Expr<'a>) -> &hir::Expr<'a> { if let hir::ExprKind::Match(expr, _, hir::MatchSource::AwaitDesugar) = expr.kind { if let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind { if matches!( func.kind, hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::IntoFutureIntoFuture, ..)) ) { return arg_0; } } } expr } /// Check whether the current expr is a function call for an IO operation fn check_io_mode(cx: &LateContext<'_>, call: &hir::Expr<'_>) -> Option { let hir::ExprKind::MethodCall(path, ..) = call.kind else { return None; }; let vectorized = match path.ident.as_str() { "write_vectored" | "read_vectored" => true, "write" | "read" => false, _ => { return None; }, }; match ( is_trait_method(cx, call, sym::IoRead), is_trait_method(cx, call, sym::IoWrite), match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCREADEXT) || match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCREADEXT), match_trait_method(cx, call, &paths::TOKIO_IO_ASYNCWRITEEXT) || match_trait_method(cx, call, &paths::FUTURES_IO_ASYNCWRITEEXT), ) { (true, _, _, _) => Some(IoOp::SyncRead(vectorized)), (_, true, _, _) => Some(IoOp::SyncWrite(vectorized)), (_, _, true, _) => Some(IoOp::AsyncRead(vectorized)), (_, _, _, true) => Some(IoOp::AsyncWrite(vectorized)), _ => None, } } fn emit_lint(cx: &LateContext<'_>, span: Span, op: IoOp, wild_cards: &[Span]) { let (msg, help) = match op { IoOp::AsyncRead(false) => ( "read amount is not handled", Some("use `AsyncReadExt::read_exact` instead, or handle partial reads"), ), IoOp::SyncRead(false) => ( "read amount is not handled", Some("use `Read::read_exact` instead, or handle partial reads"), ), IoOp::SyncWrite(false) => ( "written amount is not handled", Some("use `Write::write_all` instead, or handle partial writes"), ), IoOp::AsyncWrite(false) => ( "written amount is not handled", Some("use `AsyncWriteExt::write_all` instead, or handle partial writes"), ), IoOp::SyncRead(true) | IoOp::AsyncRead(true) => ("read amount is not handled", None), IoOp::SyncWrite(true) | IoOp::AsyncWrite(true) => ("written amount is not handled", None), }; span_lint_and_then(cx, UNUSED_IO_AMOUNT, span, msg, |diag| { if let Some(help_str) = help { diag.help(help_str); } for span in wild_cards { diag.span_note( *span, "the result is consumed here, but the amount of I/O bytes remains unhandled", ); } }); }