Skip to content

Commit 490566b

Browse files
committed
Auto merge of rust-lang#8179 - nmathewson:unused_async_io_amount, r=xFrednet
Extend unused_io_amount to cover async io. Clippy helpfully warns about code like this, telling you that you probably meant "write_all": fn say_hi<W:Write>(w: &mut W) { w.write(b"hello").unwrap(); } This patch attempts to extend the lint so it also covers this case: async fn say_hi<W:AsyncWrite>(w: &mut W) { w.write(b"hello").await.unwrap(); } (I've run into this second case several times in my own programming, and so have my coworkers, so unless we're especially accident-prone in this area, it's probably worth addressing?) Since this is my first attempt at a clippy patch, I've probably made all kinds of mistakes: please help me fix them? I'd like to learn more here. Open questions I have: * Should this be a separate lint from unused_io_amount? Maybe unused_async_io_amount? If so, how should I structure their shared code? * Should this cover tokio's AsyncWrite too? * Is it okay to write lints for stuff that isn't part of the standard library? I see that "regex" also has lints, and I figure that "futures" is probably okay too, since it's an official rust-lang repository. * What other tests are needed? * How should I improve the code? Thanks for your time! --- changelog: [`unused_io_amount`] now supports async read and write traits
2 parents 0eff589 + b6bcf0c commit 490566b

File tree

6 files changed

+227
-32
lines changed

6 files changed

+227
-32
lines changed

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ itertools = "0.10"
4747
quote = "1.0"
4848
serde = { version = "1.0", features = ["derive"] }
4949
syn = { version = "1.0", features = ["full"] }
50+
futures = "0.3"
5051
parking_lot = "0.11.2"
52+
tokio = { version = "1", features = ["io-util"] }
5153

5254
[build-dependencies]
5355
rustc_tools_util = { version = "0.2", path = "rustc_tools_util" }

clippy_lints/src/unused_io_amount.rs

+76-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clippy_utils::diagnostics::span_lint;
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
22
use clippy_utils::{is_try, match_trait_method, paths};
33
use rustc_hir as hir;
44
use rustc_lint::{LateContext, LateLintPass};
@@ -17,10 +17,17 @@ declare_clippy_lint! {
1717
/// partial-write/read, use
1818
/// `write_all`/`read_exact` instead.
1919
///
20+
/// When working with asynchronous code (either with the `futures`
21+
/// crate or with `tokio`), a similar issue exists for
22+
/// `AsyncWriteExt::write()` and `AsyncReadExt::read()` : these
23+
/// functions are also not guaranteed to process the entire
24+
/// buffer. Your code should either handle partial-writes/reads, or
25+
/// call the `write_all`/`read_exact` methods on those traits instead.
26+
///
2027
/// ### Known problems
2128
/// Detects only common patterns.
2229
///
23-
/// ### Example
30+
/// ### Examples
2431
/// ```rust,ignore
2532
/// use std::io;
2633
/// fn foo<W: io::Write>(w: &mut W) -> io::Result<()> {
@@ -68,6 +75,23 @@ impl<'tcx> LateLintPass<'tcx> for UnusedIoAmount {
6875
}
6976
}
7077

78+
/// If `expr` is an (e).await, return the inner expression "e" that's being
79+
/// waited on. Otherwise return None.
80+
fn try_remove_await<'a>(expr: &'a hir::Expr<'a>) -> Option<&hir::Expr<'a>> {
81+
if let hir::ExprKind::Match(expr, _, hir::MatchSource::AwaitDesugar) = expr.kind {
82+
if let hir::ExprKind::Call(func, [ref arg_0, ..]) = expr.kind {
83+
if matches!(
84+
func.kind,
85+
hir::ExprKind::Path(hir::QPath::LangItem(hir::LangItem::IntoFutureIntoFuture, ..))
86+
) {
87+
return Some(arg_0);
88+
}
89+
}
90+
}
91+
92+
None
93+
}
94+
7195
fn check_map_error(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>) {
7296
let mut call = call;
7397
while let hir::ExprKind::MethodCall(path, _, args, _) = call.kind {
@@ -77,30 +101,69 @@ fn check_map_error(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<
77101
break;
78102
}
79103
}
80-
check_method_call(cx, call, expr);
104+
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);
109+
}
81110
}
82111

83-
fn check_method_call(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>) {
112+
fn check_method_call(cx: &LateContext<'_>, call: &hir::Expr<'_>, expr: &hir::Expr<'_>, is_await: bool) {
84113
if let hir::ExprKind::MethodCall(path, _, _, _) = call.kind {
85114
let symbol = path.ident.as_str();
86-
let read_trait = match_trait_method(cx, call, &paths::IO_READ);
87-
let write_trait = match_trait_method(cx, call, &paths::IO_WRITE);
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+
match_trait_method(cx, call, &paths::IO_READ)
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+
match_trait_method(cx, call, &paths::IO_WRITE)
126+
};
88127

89-
match (read_trait, write_trait, symbol) {
90-
(true, _, "read") => span_lint(
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(
91138
cx,
92139
UNUSED_IO_AMOUNT,
93140
expr.span,
94-
"read amount is not handled. Use `Read::read_exact` instead",
141+
"read amount is not handled",
142+
None,
143+
"use `AsyncReadExt::read_exact` instead, or handle partial reads",
95144
),
96-
(true, _, "read_vectored") => span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "read amount is not handled"),
97-
(_, true, "write") => span_lint(
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(
98149
cx,
99150
UNUSED_IO_AMOUNT,
100151
expr.span,
101-
"written amount is not handled. Use `Write::write_all` instead",
152+
"written amount is not handled",
153+
None,
154+
"use `Write::write_all` instead, or handle partial writes",
102155
),
103-
(_, true, "write_vectored") => span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "written amount is not handled"),
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+
},
104167
_ => (),
105168
}
106169
}

clippy_utils/src/paths.rs

+8
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ pub const FROM_ITERATOR: [&str; 5] = ["core", "iter", "traits", "collect", "From
6464
pub const FROM_ITERATOR_METHOD: [&str; 6] = ["core", "iter", "traits", "collect", "FromIterator", "from_iter"];
6565
pub const FROM_STR_METHOD: [&str; 5] = ["core", "str", "traits", "FromStr", "from_str"];
6666
pub const FUTURE_FROM_GENERATOR: [&str; 3] = ["core", "future", "from_generator"];
67+
#[allow(clippy::invalid_paths)] // internal lints do not know about all external crates
68+
pub const FUTURES_IO_ASYNCREADEXT: [&str; 3] = ["futures_util", "io", "AsyncReadExt"];
69+
#[allow(clippy::invalid_paths)] // internal lints do not know about all external crates
70+
pub const FUTURES_IO_ASYNCWRITEEXT: [&str; 3] = ["futures_util", "io", "AsyncWriteExt"];
6771
pub const HASH: [&str; 3] = ["core", "hash", "Hash"];
6872
pub const HASHMAP_CONTAINS_KEY: [&str; 6] = ["std", "collections", "hash", "map", "HashMap", "contains_key"];
6973
pub const HASHMAP_ENTRY: [&str; 5] = ["std", "collections", "hash", "map", "Entry"];
@@ -194,6 +198,10 @@ pub const SYM_MODULE: [&str; 3] = ["rustc_span", "symbol", "sym"];
194198
pub const SYNTAX_CONTEXT: [&str; 3] = ["rustc_span", "hygiene", "SyntaxContext"];
195199
pub const TO_OWNED_METHOD: [&str; 4] = ["alloc", "borrow", "ToOwned", "to_owned"];
196200
pub const TO_STRING_METHOD: [&str; 4] = ["alloc", "string", "ToString", "to_string"];
201+
#[allow(clippy::invalid_paths)] // internal lints do not know about all external crates
202+
pub const TOKIO_IO_ASYNCREADEXT: [&str; 5] = ["tokio", "io", "util", "async_read_ext", "AsyncReadExt"];
203+
#[allow(clippy::invalid_paths)] // internal lints do not know about all external crates
204+
pub const TOKIO_IO_ASYNCWRITEEXT: [&str; 5] = ["tokio", "io", "util", "async_write_ext", "AsyncWriteExt"];
197205
pub const TRY_FROM: [&str; 4] = ["core", "convert", "TryFrom", "try_from"];
198206
pub const VEC_AS_MUT_SLICE: [&str; 4] = ["alloc", "vec", "Vec", "as_mut_slice"];
199207
pub const VEC_AS_SLICE: [&str; 4] = ["alloc", "vec", "Vec", "as_slice"];

tests/compile-test.rs

+6
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal-lints");
2121
static TEST_DEPENDENCIES: &[&str] = &[
2222
"clippy_utils",
2323
"derive_new",
24+
"futures",
2425
"if_chain",
2526
"itertools",
2627
"quote",
2728
"regex",
2829
"serde",
2930
"serde_derive",
3031
"syn",
32+
"tokio",
3133
"parking_lot",
3234
];
3335

@@ -38,6 +40,8 @@ extern crate clippy_utils;
3840
#[allow(unused_extern_crates)]
3941
extern crate derive_new;
4042
#[allow(unused_extern_crates)]
43+
extern crate futures;
44+
#[allow(unused_extern_crates)]
4145
extern crate if_chain;
4246
#[allow(unused_extern_crates)]
4347
extern crate itertools;
@@ -47,6 +51,8 @@ extern crate parking_lot;
4751
extern crate quote;
4852
#[allow(unused_extern_crates)]
4953
extern crate syn;
54+
#[allow(unused_extern_crates)]
55+
extern crate tokio;
5056

5157
/// Produces a string with an `--extern` flag for all UI test crate
5258
/// dependencies.

tests/ui/unused_io_amount.rs

+53
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#![allow(dead_code)]
22
#![warn(clippy::unused_io_amount)]
33

4+
extern crate futures;
5+
use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
46
use std::io::{self, Read};
57

68
fn question_mark<T: io::Read + io::Write>(s: &mut T) -> io::Result<()> {
@@ -61,4 +63,55 @@ fn combine_or(file: &str) -> Result<(), Error> {
6163
Ok(())
6264
}
6365

66+
async fn bad_async_write<W: AsyncWrite + Unpin>(w: &mut W) {
67+
w.write(b"hello world").await.unwrap();
68+
}
69+
70+
async fn bad_async_read<R: AsyncRead + Unpin>(r: &mut R) {
71+
let mut buf = [0u8; 0];
72+
r.read(&mut buf[..]).await.unwrap();
73+
}
74+
75+
async fn io_not_ignored_async_write<W: AsyncWrite + Unpin>(mut w: W) {
76+
// Here we're forgetting to await the future, so we should get a
77+
// warning about _that_ (or we would, if it were enabled), but we
78+
// won't get one about ignoring the return value.
79+
w.write(b"hello world");
80+
}
81+
82+
fn bad_async_write_closure<W: AsyncWrite + Unpin + 'static>(w: W) -> impl futures::Future<Output = io::Result<()>> {
83+
let mut w = w;
84+
async move {
85+
w.write(b"hello world").await?;
86+
Ok(())
87+
}
88+
}
89+
90+
async fn async_read_nested_or<R: AsyncRead + Unpin>(r: &mut R, do_it: bool) -> Result<[u8; 1], Error> {
91+
let mut buf = [0u8; 1];
92+
if do_it {
93+
r.read(&mut buf[..]).await.or(Err(Error::Kind))?;
94+
}
95+
Ok(buf)
96+
}
97+
98+
use tokio::io::{AsyncRead as TokioAsyncRead, AsyncReadExt as _, AsyncWrite as TokioAsyncWrite, AsyncWriteExt as _};
99+
100+
async fn bad_async_write_tokio<W: TokioAsyncWrite + Unpin>(w: &mut W) {
101+
w.write(b"hello world").await.unwrap();
102+
}
103+
104+
async fn bad_async_read_tokio<R: TokioAsyncRead + Unpin>(r: &mut R) {
105+
let mut buf = [0u8; 0];
106+
r.read(&mut buf[..]).await.unwrap();
107+
}
108+
109+
async fn undetected_bad_async_write<W: AsyncWrite + Unpin>(w: &mut W) {
110+
// It would be good to detect this case some day, but the current lint
111+
// doesn't handle it. (The documentation says that this lint "detects
112+
// only common patterns".)
113+
let future = w.write(b"Hello world");
114+
future.await.unwrap();
115+
}
116+
64117
fn main() {}

0 commit comments

Comments
 (0)