Skip to content

Commit 8b6de49

Browse files
New lint: unbuffered_bytes
1 parent 32aef11 commit 8b6de49

9 files changed

+143
-6
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6144,6 +6144,7 @@ Released 2018-09-13
61446144
[`type_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_complexity
61456145
[`type_id_on_box`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_id_on_box
61466146
[`type_repetition_in_bounds`]: https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds
6147+
[`unbuffered_bytes`]: https://rust-lang.github.io/rust-clippy/master/index.html#unbuffered_bytes
61476148
[`unchecked_duration_subtraction`]: https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_duration_subtraction
61486149
[`unconditional_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#unconditional_recursion
61496150
[`undocumented_unsafe_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#undocumented_unsafe_blocks

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
482482
crate::methods::SUSPICIOUS_SPLITN_INFO,
483483
crate::methods::SUSPICIOUS_TO_OWNED_INFO,
484484
crate::methods::TYPE_ID_ON_BOX_INFO,
485+
crate::methods::UNBUFFERED_BYTES_INFO,
485486
crate::methods::UNINIT_ASSUMED_INIT_INFO,
486487
crate::methods::UNIT_HASH_INFO,
487488
crate::methods::UNNECESSARY_FALLIBLE_CONVERSIONS_INFO,

clippy_lints/src/methods/mod.rs

+30
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ mod suspicious_map;
113113
mod suspicious_splitn;
114114
mod suspicious_to_owned;
115115
mod type_id_on_box;
116+
mod unbuffered_bytes;
116117
mod uninit_assumed_init;
117118
mod unit_hash;
118119
mod unnecessary_fallible_conversions;
@@ -4406,6 +4407,33 @@ declare_clippy_lint! {
44064407
"using `Option::and_then` or `Result::and_then` to chain a computation that returns an `Option` or a `Result`"
44074408
}
44084409

4410+
declare_clippy_lint! {
4411+
/// ### What it does
4412+
/// Checks for calls to `Read::bytes` on types which don't implement `BufRead`.
4413+
///
4414+
/// ### Why is this bad?
4415+
/// The default implementation calls `read` for each byte, which can be very inefficient for data that’s not in memory, such as `File`.
4416+
///
4417+
/// ### Example
4418+
/// ```no_run
4419+
/// use std::io::Read;
4420+
/// use std::fs::File;
4421+
/// let file = File::open("./bytes.txt").unwrap();
4422+
/// file.bytes();
4423+
/// ```
4424+
/// Use instead:
4425+
/// ```no_run
4426+
/// use std::io::{BufReader, Read};
4427+
/// use std::fs::File;
4428+
/// let file = BufReader::new(std::fs::File::open("./bytes.txt").unwrap());
4429+
/// file.bytes();
4430+
/// ```
4431+
#[clippy::version = "1.86.0"]
4432+
pub UNBUFFERED_BYTES,
4433+
perf,
4434+
"calling .bytes() is very inefficient when data is not in memory"
4435+
}
4436+
44094437
#[expect(clippy::struct_excessive_bools)]
44104438
pub struct Methods {
44114439
avoid_breaking_exported_api: bool,
@@ -4580,6 +4608,7 @@ impl_lint_pass!(Methods => [
45804608
MANUAL_REPEAT_N,
45814609
SLICED_STRING_AS_BYTES,
45824610
RETURN_AND_THEN,
4611+
UNBUFFERED_BYTES,
45834612
]);
45844613

45854614
/// Extracts a method call name, args, and `Span` of the method name.
@@ -4856,6 +4885,7 @@ impl Methods {
48564885
("as_ptr", []) => manual_c_str_literals::check_as_ptr(cx, expr, recv, &self.msrv),
48574886
("as_ref", []) => useless_asref::check(cx, expr, "as_ref", recv),
48584887
("assume_init", []) => uninit_assumed_init::check(cx, expr, recv),
4888+
("bytes", []) => unbuffered_bytes::check(cx, expr, recv),
48594889
("cloned", []) => {
48604890
cloned_instead_of_copied::check(cx, expr, recv, span, &self.msrv);
48614891
option_as_ref_cloned::check(cx, recv, span);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use super::UNBUFFERED_BYTES;
2+
use clippy_utils::diagnostics::span_lint_and_help;
3+
use clippy_utils::ty::implements_trait;
4+
use clippy_utils::{get_trait_def_id, is_trait_method, paths};
5+
use rustc_hir as hir;
6+
use rustc_lint::LateContext;
7+
use rustc_span::sym;
8+
9+
pub(super) fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) {
10+
let ty = cx.typeck_results().expr_ty_adjusted(recv);
11+
12+
// If the .bytes() call is a call from the Read trait
13+
if is_trait_method(cx, expr, sym::IoRead) {
14+
// Retrieve the DefId of the BufRead trait
15+
// FIXME: add a diagnostic item for `BufRead`
16+
let Some(buf_read) = get_trait_def_id(cx.tcx, &paths::BUF_READ) else {
17+
return;
18+
};
19+
// And the implementor of the trait is not buffered
20+
if !implements_trait(cx, ty, buf_read, &[]) {
21+
span_lint_and_help(
22+
cx,
23+
UNBUFFERED_BYTES,
24+
expr.span,
25+
"calling .bytes() is very inefficient when data is not in memory",
26+
None,
27+
"consider using `BufReader`",
28+
);
29+
}
30+
}
31+
}

clippy_utils/src/paths.rs

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub const SYNTAX_CONTEXT: [&str; 3] = ["rustc_span", "hygiene", "SyntaxContext"]
2929

3030
// Paths in `core`/`alloc`/`std`. This should be avoided and cleaned up by adding diagnostic items.
3131
pub const ABORT: [&str; 3] = ["std", "process", "abort"];
32+
pub const BUF_READ: [&str; 3] = ["std", "io", "BufRead"];
3233
pub const CHILD: [&str; 3] = ["std", "process", "Child"];
3334
pub const CHILD_ID: [&str; 4] = ["std", "process", "Child", "id"];
3435
pub const CHILD_KILL: [&str; 4] = ["std", "process", "Child", "kill"];

tests/ui/bytes_count_to_len.fixed

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![warn(clippy::bytes_count_to_len)]
22
use std::fs::File;
3-
use std::io::Read;
3+
use std::io::{BufReader, Read};
44

55
fn main() {
66
// should fix, because type is String
@@ -26,8 +26,8 @@ fn main() {
2626
bytes.bytes().count();
2727

2828
// The type is File, so should not fix
29-
let _ = File::open("foobar").unwrap().bytes().count();
29+
let _ = BufReader::new(File::open("foobar").unwrap()).bytes().count();
3030

31-
let f = File::open("foobar").unwrap();
31+
let f = BufReader::new(File::open("foobar").unwrap());
3232
let _ = f.bytes().count();
3333
}

tests/ui/bytes_count_to_len.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![warn(clippy::bytes_count_to_len)]
22
use std::fs::File;
3-
use std::io::Read;
3+
use std::io::{BufReader, Read};
44

55
fn main() {
66
// should fix, because type is String
@@ -26,8 +26,8 @@ fn main() {
2626
bytes.bytes().count();
2727

2828
// The type is File, so should not fix
29-
let _ = File::open("foobar").unwrap().bytes().count();
29+
let _ = BufReader::new(File::open("foobar").unwrap()).bytes().count();
3030

31-
let f = File::open("foobar").unwrap();
31+
let f = BufReader::new(File::open("foobar").unwrap());
3232
let _ = f.bytes().count();
3333
}

tests/ui/unbuffered_bytes.rs

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#![warn(clippy::unbuffered_bytes)]
2+
3+
use std::fs::File;
4+
use std::io::{BufReader, Cursor, Read, Stdin, stdin};
5+
use std::net::TcpStream;
6+
7+
fn main() {
8+
// File is not buffered, should complain
9+
let file = File::open("./bytes.txt").unwrap();
10+
file.bytes();
11+
12+
// TcpStream is not buffered, should complain
13+
let tcp_stream: TcpStream = TcpStream::connect("127.0.0.1:80").unwrap();
14+
tcp_stream.bytes();
15+
16+
// BufReader<File> is buffered, should not complain
17+
let file = BufReader::new(File::open("./bytes.txt").unwrap());
18+
file.bytes();
19+
20+
// Cursor is buffered, should not complain
21+
let cursor = Cursor::new(Vec::new());
22+
cursor.bytes();
23+
24+
// Stdio would acquire the lock for every byte, should complain
25+
let s: Stdin = stdin();
26+
s.bytes();
27+
28+
// But when locking stdin, this is fine so should not complain
29+
let s: Stdin = stdin();
30+
let s = s.lock();
31+
s.bytes();
32+
}
33+
34+
fn use_read<R: Read>(r: R) {
35+
// Callers of `use_read` may choose a `R` that is not buffered
36+
r.bytes();
37+
}

tests/ui/unbuffered_bytes.stderr

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
error: calling .bytes() is very inefficient when data is not in memory
2+
--> tests/ui/unbuffered_bytes.rs:10:5
3+
|
4+
LL | file.bytes();
5+
| ^^^^^^^^^^^^
6+
|
7+
= help: consider using `BufReader`
8+
= note: `-D clippy::unbuffered-bytes` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::unbuffered_bytes)]`
10+
11+
error: calling .bytes() is very inefficient when data is not in memory
12+
--> tests/ui/unbuffered_bytes.rs:14:5
13+
|
14+
LL | tcp_stream.bytes();
15+
| ^^^^^^^^^^^^^^^^^^
16+
|
17+
= help: consider using `BufReader`
18+
19+
error: calling .bytes() is very inefficient when data is not in memory
20+
--> tests/ui/unbuffered_bytes.rs:26:5
21+
|
22+
LL | s.bytes();
23+
| ^^^^^^^^^
24+
|
25+
= help: consider using `BufReader`
26+
27+
error: calling .bytes() is very inefficient when data is not in memory
28+
--> tests/ui/unbuffered_bytes.rs:36:5
29+
|
30+
LL | r.bytes();
31+
| ^^^^^^^^^
32+
|
33+
= help: consider using `BufReader`
34+
35+
error: aborting due to 4 previous errors
36+

0 commit comments

Comments
 (0)