Skip to content

Commit 65d1f83

Browse files
committed
Extend [unused_io_amount] to cover AsyncRead and AsyncWrite.
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?) This patch covers the Async{Read,Write}Ext traits in futures-rs, and in tokio, since both are quite widely used. changelog: [`unused_io_amount`] now supports AsyncReadExt and AsyncWriteExt.
1 parent 0eff589 commit 65d1f83

File tree

6 files changed

+181
-21
lines changed

6 files changed

+181
-21
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
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

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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,61 @@ 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(
91130
cx,
92131
UNUSED_IO_AMOUNT,
93132
expr.span,
94133
"read amount is not handled. Use `Read::read_exact` instead",
95134
),
96-
(true, _, "read_vectored") => span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "read amount is not handled"),
97-
(_, true, "write") => span_lint(
135+
(true, _, "read", true) => span_lint(
136+
cx,
137+
UNUSED_IO_AMOUNT,
138+
expr.span,
139+
"read amount is not handled. Use `AsyncReadExt::read_exact` instead",
140+
),
141+
(true, _, "read_vectored", _) => {
142+
span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "read amount is not handled");
143+
},
144+
(_, true, "write", false) => span_lint(
98145
cx,
99146
UNUSED_IO_AMOUNT,
100147
expr.span,
101148
"written amount is not handled. Use `Write::write_all` instead",
102149
),
103-
(_, true, "write_vectored") => span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "written amount is not handled"),
150+
(_, true, "write", true) => span_lint(
151+
cx,
152+
UNUSED_IO_AMOUNT,
153+
expr.span,
154+
"written amount is not handled. Use `AsyncWriteExt::write_all` instead",
155+
),
156+
(_, true, "write_vectored", _) => {
157+
span_lint(cx, UNUSED_IO_AMOUNT, expr.span, "written amount is not handled");
158+
},
104159
_ => (),
105160
}
106161
}

clippy_utils/src/paths.rs

Lines changed: 8 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 53 additions & 0 deletions
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() {}

tests/ui/unused_io_amount.stderr

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,61 @@
11
error: written amount is not handled. Use `Write::write_all` instead
2-
--> $DIR/unused_io_amount.rs:7:5
2+
--> $DIR/unused_io_amount.rs:9:5
33
|
44
LL | s.write(b"test")?;
55
| ^^^^^^^^^^^^^^^^^
66
|
77
= note: `-D clippy::unused-io-amount` implied by `-D warnings`
88

99
error: read amount is not handled. Use `Read::read_exact` instead
10-
--> $DIR/unused_io_amount.rs:9:5
10+
--> $DIR/unused_io_amount.rs:11:5
1111
|
1212
LL | s.read(&mut buf)?;
1313
| ^^^^^^^^^^^^^^^^^
1414

1515
error: written amount is not handled. Use `Write::write_all` instead
16-
--> $DIR/unused_io_amount.rs:14:5
16+
--> $DIR/unused_io_amount.rs:16:5
1717
|
1818
LL | s.write(b"test").unwrap();
1919
| ^^^^^^^^^^^^^^^^^^^^^^^^^
2020

2121
error: read amount is not handled. Use `Read::read_exact` instead
22-
--> $DIR/unused_io_amount.rs:16:5
22+
--> $DIR/unused_io_amount.rs:18:5
2323
|
2424
LL | s.read(&mut buf).unwrap();
2525
| ^^^^^^^^^^^^^^^^^^^^^^^^^
2626

2727
error: read amount is not handled
28-
--> $DIR/unused_io_amount.rs:20:5
28+
--> $DIR/unused_io_amount.rs:22:5
2929
|
3030
LL | s.read_vectored(&mut [io::IoSliceMut::new(&mut [])])?;
3131
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3232

3333
error: written amount is not handled
34-
--> $DIR/unused_io_amount.rs:21:5
34+
--> $DIR/unused_io_amount.rs:23:5
3535
|
3636
LL | s.write_vectored(&[io::IoSlice::new(&[])])?;
3737
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3838

3939
error: read amount is not handled. Use `Read::read_exact` instead
40-
--> $DIR/unused_io_amount.rs:28:5
40+
--> $DIR/unused_io_amount.rs:30:5
4141
|
4242
LL | reader.read(&mut result).ok()?;
4343
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4444

4545
error: read amount is not handled. Use `Read::read_exact` instead
46-
--> $DIR/unused_io_amount.rs:37:5
46+
--> $DIR/unused_io_amount.rs:39:5
4747
|
4848
LL | reader.read(&mut result).or_else(|err| Err(err))?;
4949
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5050

5151
error: read amount is not handled. Use `Read::read_exact` instead
52-
--> $DIR/unused_io_amount.rs:49:5
52+
--> $DIR/unused_io_amount.rs:51:5
5353
|
5454
LL | reader.read(&mut result).or(Err(Error::Kind))?;
5555
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5656

5757
error: read amount is not handled. Use `Read::read_exact` instead
58-
--> $DIR/unused_io_amount.rs:56:5
58+
--> $DIR/unused_io_amount.rs:58:5
5959
|
6060
LL | / reader
6161
LL | | .read(&mut result)
@@ -64,5 +64,41 @@ LL | | .or(Err(Error::Kind))
6464
LL | | .expect("error");
6565
| |________________________^
6666

67-
error: aborting due to 10 previous errors
67+
error: written amount is not handled. Use `AsyncWriteExt::write_all` instead
68+
--> $DIR/unused_io_amount.rs:67:5
69+
|
70+
LL | w.write(b"hello world").await.unwrap();
71+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
73+
error: read amount is not handled. Use `AsyncReadExt::read_exact` instead
74+
--> $DIR/unused_io_amount.rs:72:5
75+
|
76+
LL | r.read(&mut buf[..]).await.unwrap();
77+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78+
79+
error: written amount is not handled. Use `AsyncWriteExt::write_all` instead
80+
--> $DIR/unused_io_amount.rs:85:9
81+
|
82+
LL | w.write(b"hello world").await?;
83+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84+
85+
error: read amount is not handled. Use `AsyncReadExt::read_exact` instead
86+
--> $DIR/unused_io_amount.rs:93:9
87+
|
88+
LL | r.read(&mut buf[..]).await.or(Err(Error::Kind))?;
89+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90+
91+
error: written amount is not handled. Use `AsyncWriteExt::write_all` instead
92+
--> $DIR/unused_io_amount.rs:101:5
93+
|
94+
LL | w.write(b"hello world").await.unwrap();
95+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96+
97+
error: read amount is not handled. Use `AsyncReadExt::read_exact` instead
98+
--> $DIR/unused_io_amount.rs:106:5
99+
|
100+
LL | r.read(&mut buf[..]).await.unwrap();
101+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
102+
103+
error: aborting due to 16 previous errors
68104

0 commit comments

Comments
 (0)