Skip to content

Commit eb59e32

Browse files
committed
Flag bufreader.lines().filter_map(Result::ok) as suspicious
1 parent 1d1e723 commit eb59e32

File tree

8 files changed

+151
-0
lines changed

8 files changed

+151
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4645,6 +4645,7 @@ Released 2018-09-13
46454645
[`let_underscore_untyped`]: https://rust-lang.github.io/rust-clippy/master/index.html#let_underscore_untyped
46464646
[`let_unit_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#let_unit_value
46474647
[`let_with_type_underscore`]: https://rust-lang.github.io/rust-clippy/master/index.html#let_with_type_underscore
4648+
[`lines_filter_map_ok`]: https://rust-lang.github.io/rust-clippy/master/index.html#lines_filter_map_ok
46484649
[`linkedlist`]: https://rust-lang.github.io/rust-clippy/master/index.html#linkedlist
46494650
[`logic_bug`]: https://rust-lang.github.io/rust-clippy/master/index.html#logic_bug
46504651
[`lossy_float_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#lossy_float_literal

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
231231
crate::let_with_type_underscore::LET_WITH_TYPE_UNDERSCORE_INFO,
232232
crate::lifetimes::EXTRA_UNUSED_LIFETIMES_INFO,
233233
crate::lifetimes::NEEDLESS_LIFETIMES_INFO,
234+
crate::lines_filter_map_ok::LINES_FILTER_MAP_OK_INFO,
234235
crate::literal_representation::DECIMAL_LITERAL_REPRESENTATION_INFO,
235236
crate::literal_representation::INCONSISTENT_DIGIT_GROUPING_INFO,
236237
crate::literal_representation::LARGE_DIGIT_GROUPS_INFO,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ mod let_if_seq;
170170
mod let_underscore;
171171
mod let_with_type_underscore;
172172
mod lifetimes;
173+
mod lines_filter_map_ok;
173174
mod literal_representation;
174175
mod loops;
175176
mod macro_use;
@@ -938,6 +939,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
938939
store.register_late_pass(|_| Box::new(let_with_type_underscore::UnderscoreTyped));
939940
store.register_late_pass(|_| Box::new(allow_attributes::AllowAttribute));
940941
store.register_late_pass(move |_| Box::new(manual_main_separator_str::ManualMainSeparatorStr::new(msrv())));
942+
store.register_late_pass(|_| Box::new(lines_filter_map_ok::LinesFilterMapOk));
941943
// add lints here, do not remove this comment, it's used in `new_lint`
942944
}
943945

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use clippy_utils::{diagnostics::span_lint_and_then, is_trait_method, match_def_path, match_trait_method, paths};
2+
use rustc_errors::Applicability;
3+
use rustc_hir::{Expr, ExprKind};
4+
use rustc_lint::{LateContext, LateLintPass};
5+
use rustc_session::{declare_lint_pass, declare_tool_lint};
6+
use rustc_span::sym;
7+
8+
declare_clippy_lint! {
9+
/// ### What it does
10+
/// Detect uses of `b.lines().filter_map(Result::ok)` or `b.lines().flat_map(Result::ok)`
11+
/// when `b` implements `BufRead`.
12+
///
13+
/// ### Why is this bad?
14+
/// `b.lines()` might produce an infinite stream of `Err` and `filter_map(Result::ok)`
15+
/// will also run forever. Using `map_while(Result::ok)` would stop after the first
16+
/// `Err` is emitted.
17+
///
18+
/// For example, on some platforms, `std::fs::File::open(path)` might return `Ok(fs)`
19+
/// when `path` is a directory, and reading from `fs` will then return an error.
20+
/// If `fs` was inadvertently put in a `BufReader`, calling `lines()` on it would
21+
/// return an infinite stream of `Err` variants.
22+
///
23+
/// ### Example
24+
/// ```rust
25+
/// #use std::{fs::File, io::{BufRead, BufReader}};
26+
/// let reader = BufReader::new(File::open("some path")?);
27+
/// for line in reader.lines().flat_map(Result::ok) {
28+
/// println!("{line}");
29+
/// }
30+
/// ```
31+
/// Use instead:
32+
/// ```rust
33+
/// #use std::{fs::File, io::{BufRead, BufReader}};
34+
/// let reader = BufReader::new(File::open("some path")?);
35+
/// for line in reader.lines().map_while(Result::ok) {
36+
/// println!("{line}");
37+
/// }
38+
/// ```
39+
#[clippy::version = "1.70.0"]
40+
pub LINES_FILTER_MAP_OK,
41+
nursery,
42+
"`BufRead::lines()` might produce an infinite stream of errors"
43+
}
44+
declare_lint_pass!(LinesFilterMapOk => [LINES_FILTER_MAP_OK]);
45+
46+
impl LateLintPass<'_> for LinesFilterMapOk {
47+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
48+
if let ExprKind::MethodCall(fm_method, fm_receiver, [fm_arg], fm_span) = expr.kind &&
49+
is_trait_method(cx, expr, sym::Iterator) &&
50+
(fm_method.ident.as_str() == "filter_map" || fm_method.ident.as_str() == "flat_map") &&
51+
let ExprKind::MethodCall(lines_method, _, &[], _) = fm_receiver.kind &&
52+
match_trait_method(cx, fm_receiver, &paths::STD_IO_BUFREAD) &&
53+
lines_method.ident.as_str() == "lines" &&
54+
let ExprKind::Path(qpath) = &fm_arg.kind &&
55+
let Some(did) = cx.qpath_res(qpath, fm_arg.hir_id).opt_def_id() &&
56+
match_def_path(cx, did, &paths::CORE_RESULT_OK_METHOD)
57+
{
58+
span_lint_and_then(cx,
59+
LINES_FILTER_MAP_OK,
60+
fm_span,
61+
&format!("`{}()` will run forever on an infinite stream of `Err` returned by `lines()`", fm_method.ident),
62+
|diag| {
63+
diag.span_note(
64+
fm_receiver.span,
65+
"`BufRead::lines()` may produce an infinite stream of `Err` in case of a read error");
66+
diag.span_suggestion(fm_span, "replace with", "map_while(Result::ok)", Applicability::MaybeIncorrect);
67+
});
68+
}
69+
}
70+
}

clippy_utils/src/paths.rs

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub const CLONE_TRAIT_METHOD: [&str; 4] = ["core", "clone", "Clone", "clone"];
2323
pub const CORE_ITER_CLONED: [&str; 6] = ["core", "iter", "traits", "iterator", "Iterator", "cloned"];
2424
pub const CORE_ITER_COPIED: [&str; 6] = ["core", "iter", "traits", "iterator", "Iterator", "copied"];
2525
pub const CORE_ITER_FILTER: [&str; 6] = ["core", "iter", "traits", "iterator", "Iterator", "filter"];
26+
pub const CORE_RESULT_OK_METHOD: [&str; 4] = ["core", "result", "Result", "ok"];
2627
pub const CSTRING_AS_C_STR: [&str; 5] = ["alloc", "ffi", "c_str", "CString", "as_c_str"];
2728
pub const DEFAULT_TRAIT_METHOD: [&str; 4] = ["core", "default", "Default", "default"];
2829
pub const DEREF_MUT_TRAIT_METHOD: [&str; 5] = ["core", "ops", "deref", "DerefMut", "deref_mut"];
@@ -113,6 +114,7 @@ pub const STDERR: [&str; 4] = ["std", "io", "stdio", "stderr"];
113114
pub const STDOUT: [&str; 4] = ["std", "io", "stdio", "stdout"];
114115
pub const CONVERT_IDENTITY: [&str; 3] = ["core", "convert", "identity"];
115116
pub const STD_FS_CREATE_DIR: [&str; 3] = ["std", "fs", "create_dir"];
117+
pub const STD_IO_BUFREAD: [&str; 3] = ["std", "io", "BufRead"];
116118
pub const STD_IO_SEEK: [&str; 3] = ["std", "io", "Seek"];
117119
pub const STD_IO_SEEK_FROM_CURRENT: [&str; 4] = ["std", "io", "SeekFrom", "Current"];
118120
pub const STD_IO_SEEKFROM_START: [&str; 4] = ["std", "io", "SeekFrom", "Start"];

tests/ui/lines_filter_map_ok.fixed

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::lines_filter_map_ok)]
5+
6+
use std::io::{self, BufRead, BufReader};
7+
8+
fn main() -> io::Result<()> {
9+
let f = std::fs::File::open("/tmp/t.rs")?;
10+
// Lint
11+
BufReader::new(f)
12+
.lines()
13+
.map_while(Result::ok)
14+
.for_each(|l| println!("{l}"));
15+
// Lint
16+
let f = std::fs::File::open("/tmp/t.rs")?;
17+
BufReader::new(f)
18+
.lines()
19+
.map_while(Result::ok)
20+
.for_each(|l| println!("{l}"));
21+
let s = "foo\nbar\nbaz\n";
22+
Ok(())
23+
}

tests/ui/lines_filter_map_ok.rs

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::lines_filter_map_ok)]
5+
6+
use std::io::{self, BufRead, BufReader};
7+
8+
fn main() -> io::Result<()> {
9+
let f = std::fs::File::open("/tmp/t.rs")?;
10+
// Lint
11+
BufReader::new(f)
12+
.lines()
13+
.filter_map(Result::ok)
14+
.for_each(|l| println!("{l}"));
15+
// Lint
16+
let f = std::fs::File::open("/tmp/t.rs")?;
17+
BufReader::new(f)
18+
.lines()
19+
.flat_map(Result::ok)
20+
.for_each(|l| println!("{l}"));
21+
let s = "foo\nbar\nbaz\n";
22+
Ok(())
23+
}

tests/ui/lines_filter_map_ok.stderr

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
error: `filter_map()` will run forever on an infinite stream of `Err` returned by `lines()`
2+
--> $DIR/lines_filter_map_ok.rs:13:10
3+
|
4+
LL | .filter_map(Result::ok)
5+
| ^^^^^^^^^^^^^^^^^^^^^^ help: replace with: `map_while(Result::ok)`
6+
|
7+
note: `BufRead::lines()` may produce an infinite stream of `Err` in case of a read error
8+
--> $DIR/lines_filter_map_ok.rs:11:5
9+
|
10+
LL | / BufReader::new(f)
11+
LL | | .lines()
12+
| |________________^
13+
= note: `-D clippy::lines-filter-map-ok` implied by `-D warnings`
14+
15+
error: `flat_map()` will run forever on an infinite stream of `Err` returned by `lines()`
16+
--> $DIR/lines_filter_map_ok.rs:19:10
17+
|
18+
LL | .flat_map(Result::ok)
19+
| ^^^^^^^^^^^^^^^^^^^^ help: replace with: `map_while(Result::ok)`
20+
|
21+
note: `BufRead::lines()` may produce an infinite stream of `Err` in case of a read error
22+
--> $DIR/lines_filter_map_ok.rs:17:5
23+
|
24+
LL | / BufReader::new(f)
25+
LL | | .lines()
26+
| |________________^
27+
28+
error: aborting due to 2 previous errors
29+

0 commit comments

Comments
 (0)