Skip to content

Commit 1e656d8

Browse files
committed
Auto merge of rust-lang#10970 - y21:read_line_without_trim, r=giraffate
new lint: `read_line_without_trim` This adds a new lint that checks for calls to `Stdin::read_line` with a reference to a string that is then attempted to parse into an integer type without first trimming it, which is always going to fail at runtime. This is something that I've seen happen a lot to beginners, because it's easy to run into when following the example of chapter 2 in the book where it shows how to program a guessing game. It would be nice if we could point beginners to clippy and tell them "let's see what clippy has to say" and have clippy explain to them why it fails 👀 I think this lint can later be "generalized" to work not just for `Stdin` but also any `BufRead` (which seems to be where the guarantee about the trailing newline comes from) and also, matching/comparing it to a string slice that doesn't end in a newline character (e.g. `input == "foo"` is always going to fail) changelog: new lint: [`read_line_without_trim`]
2 parents 3f4e599 + 85f535b commit 1e656d8

7 files changed

+255
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5134,6 +5134,7 @@ Released 2018-09-13
51345134
[`rc_buffer`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer
51355135
[`rc_clone_in_vec_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_clone_in_vec_init
51365136
[`rc_mutex`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex
5137+
[`read_line_without_trim`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_line_without_trim
51375138
[`read_zero_byte_vec`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_zero_byte_vec
51385139
[`recursive_format_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#recursive_format_impl
51395140
[`redundant_allocation`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_allocation

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
389389
crate::methods::OR_THEN_UNWRAP_INFO,
390390
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
391391
crate::methods::RANGE_ZIP_WITH_LEN_INFO,
392+
crate::methods::READ_LINE_WITHOUT_TRIM_INFO,
392393
crate::methods::REPEAT_ONCE_INFO,
393394
crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO,
394395
crate::methods::SEARCH_IS_SOME_INFO,

clippy_lints/src/methods/mod.rs

+34
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ mod or_fun_call;
7272
mod or_then_unwrap;
7373
mod path_buf_push_overwrite;
7474
mod range_zip_with_len;
75+
mod read_line_without_trim;
7576
mod repeat_once;
7677
mod search_is_some;
7778
mod seek_from_current;
@@ -3348,6 +3349,35 @@ declare_clippy_lint! {
33483349
"checks for usage of `Iterator::fold` with a type that implements `Try`"
33493350
}
33503351

3352+
declare_clippy_lint! {
3353+
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
3354+
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
3355+
/// always fail because the string has a trailing newline in it.
3356+
///
3357+
/// ### Why is this bad?
3358+
/// The `.parse()` call will always fail.
3359+
///
3360+
/// ### Example
3361+
/// ```rust,ignore
3362+
/// let mut input = String::new();
3363+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3364+
/// let num: i32 = input.parse().expect("Not a number!");
3365+
/// assert_eq!(num, 42); // we never even get here!
3366+
/// ```
3367+
/// Use instead:
3368+
/// ```rust,ignore
3369+
/// let mut input = String::new();
3370+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3371+
/// let num: i32 = input.trim_end().parse().expect("Not a number!");
3372+
/// // ^^^^^^^^^^^ remove the trailing newline
3373+
/// assert_eq!(num, 42);
3374+
/// ```
3375+
#[clippy::version = "1.72.0"]
3376+
pub READ_LINE_WITHOUT_TRIM,
3377+
correctness,
3378+
"calling `Stdin::read_line`, then trying to parse it without first trimming"
3379+
}
3380+
33513381
pub struct Methods {
33523382
avoid_breaking_exported_api: bool,
33533383
msrv: Msrv,
@@ -3468,6 +3498,7 @@ impl_lint_pass!(Methods => [
34683498
REPEAT_ONCE,
34693499
STABLE_SORT_PRIMITIVE,
34703500
UNIT_HASH,
3501+
READ_LINE_WITHOUT_TRIM,
34713502
UNNECESSARY_SORT_BY,
34723503
VEC_RESIZE_TO_ZERO,
34733504
VERBOSE_FILE_READS,
@@ -3879,6 +3910,9 @@ impl Methods {
38793910
("read_to_string", [_]) => {
38803911
verbose_file_reads::check(cx, expr, recv, verbose_file_reads::READ_TO_STRING_MSG);
38813912
},
3913+
("read_line", [arg]) => {
3914+
read_line_without_trim::check(cx, expr, recv, arg);
3915+
}
38823916
("repeat", [arg]) => {
38833917
repeat_once::check(cx, expr, recv, arg);
38843918
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use std::ops::ControlFlow;
2+
3+
use clippy_utils::{
4+
diagnostics::span_lint_and_then, get_parent_expr, match_def_path, source::snippet, ty::is_type_diagnostic_item,
5+
visitors::for_each_local_use_after_expr,
6+
};
7+
use rustc_errors::Applicability;
8+
use rustc_hir::Expr;
9+
use rustc_hir::QPath;
10+
use rustc_hir::{def::Res, ExprKind};
11+
use rustc_lint::LateContext;
12+
use rustc_middle::ty::{self, Ty};
13+
use rustc_span::sym;
14+
15+
use super::READ_LINE_WITHOUT_TRIM;
16+
17+
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
18+
fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool {
19+
// only allow a very limited set of types for now, for which we 100% know parsing will fail
20+
matches!(ty.kind(), ty::Float(_) | ty::Bool | ty::Int(_) | ty::Uint(_))
21+
}
22+
23+
pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) {
24+
if let Some(recv_adt) = cx.typeck_results().expr_ty(recv).ty_adt_def()
25+
&& match_def_path(cx, recv_adt.did(), &["std", "io", "stdio", "Stdin"])
26+
&& let ExprKind::Path(QPath::Resolved(_, path)) = arg.peel_borrows().kind
27+
&& let Res::Local(local_id) = path.res
28+
{
29+
// We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
30+
// now let's check if the first use of the string passed to `::read_line()` is
31+
// parsed into a type that will always fail if it has a trailing newline.
32+
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
33+
if let Some(parent) = get_parent_expr(cx, expr)
34+
&& let ExprKind::MethodCall(segment, .., span) = parent.kind
35+
&& segment.ident.name == sym!(parse)
36+
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
37+
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
38+
&& let ty::Adt(_, substs) = parse_result_ty.kind()
39+
&& let Some(ok_ty) = substs[0].as_type()
40+
&& parse_fails_on_trailing_newline(ok_ty)
41+
{
42+
let local_snippet = snippet(cx, expr.span, "<expr>");
43+
span_lint_and_then(
44+
cx,
45+
READ_LINE_WITHOUT_TRIM,
46+
span,
47+
"calling `.parse()` without trimming the trailing newline character",
48+
|diag| {
49+
diag.span_note(call.span, "call to `.read_line()` here, \
50+
which leaves a trailing newline character in the buffer, \
51+
which in turn will cause `.parse()` to fail");
52+
53+
diag.span_suggestion(
54+
expr.span,
55+
"try",
56+
format!("{local_snippet}.trim_end()"),
57+
Applicability::MachineApplicable,
58+
);
59+
}
60+
);
61+
}
62+
63+
// only consider the first use to prevent this scenario:
64+
// ```
65+
// let mut s = String::new();
66+
// std::io::stdin().read_line(&mut s);
67+
// s.pop();
68+
// let _x: i32 = s.parse().unwrap();
69+
// ```
70+
// this is actually fine, because the pop call removes the trailing newline.
71+
ControlFlow::<(), ()>::Break(())
72+
});
73+
}
74+
}

tests/ui/read_line_without_trim.fixed

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//@run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::read_line_without_trim)]
5+
6+
fn main() {
7+
let mut input = String::new();
8+
std::io::stdin().read_line(&mut input).unwrap();
9+
input.pop();
10+
let _x: i32 = input.parse().unwrap(); // don't trigger here, newline character is popped
11+
12+
let mut input = String::new();
13+
std::io::stdin().read_line(&mut input).unwrap();
14+
let _x: i32 = input.trim_end().parse().unwrap();
15+
16+
let mut input = String::new();
17+
std::io::stdin().read_line(&mut input).unwrap();
18+
let _x = input.trim_end().parse::<i32>().unwrap();
19+
20+
let mut input = String::new();
21+
std::io::stdin().read_line(&mut input).unwrap();
22+
let _x = input.trim_end().parse::<u32>().unwrap();
23+
24+
let mut input = String::new();
25+
std::io::stdin().read_line(&mut input).unwrap();
26+
let _x = input.trim_end().parse::<f32>().unwrap();
27+
28+
let mut input = String::new();
29+
std::io::stdin().read_line(&mut input).unwrap();
30+
let _x = input.trim_end().parse::<bool>().unwrap();
31+
32+
let mut input = String::new();
33+
std::io::stdin().read_line(&mut input).unwrap();
34+
// this is actually ok, so don't lint here
35+
let _x = input.parse::<String>().unwrap();
36+
}

tests/ui/read_line_without_trim.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//@run-rustfix
2+
3+
#![allow(unused)]
4+
#![warn(clippy::read_line_without_trim)]
5+
6+
fn main() {
7+
let mut input = String::new();
8+
std::io::stdin().read_line(&mut input).unwrap();
9+
input.pop();
10+
let _x: i32 = input.parse().unwrap(); // don't trigger here, newline character is popped
11+
12+
let mut input = String::new();
13+
std::io::stdin().read_line(&mut input).unwrap();
14+
let _x: i32 = input.parse().unwrap();
15+
16+
let mut input = String::new();
17+
std::io::stdin().read_line(&mut input).unwrap();
18+
let _x = input.parse::<i32>().unwrap();
19+
20+
let mut input = String::new();
21+
std::io::stdin().read_line(&mut input).unwrap();
22+
let _x = input.parse::<u32>().unwrap();
23+
24+
let mut input = String::new();
25+
std::io::stdin().read_line(&mut input).unwrap();
26+
let _x = input.parse::<f32>().unwrap();
27+
28+
let mut input = String::new();
29+
std::io::stdin().read_line(&mut input).unwrap();
30+
let _x = input.parse::<bool>().unwrap();
31+
32+
let mut input = String::new();
33+
std::io::stdin().read_line(&mut input).unwrap();
34+
// this is actually ok, so don't lint here
35+
let _x = input.parse::<String>().unwrap();
36+
}
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
error: calling `.parse()` without trimming the trailing newline character
2+
--> $DIR/read_line_without_trim.rs:14:25
3+
|
4+
LL | let _x: i32 = input.parse().unwrap();
5+
| ----- ^^^^^^^
6+
| |
7+
| help: try: `input.trim_end()`
8+
|
9+
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
10+
--> $DIR/read_line_without_trim.rs:13:5
11+
|
12+
LL | std::io::stdin().read_line(&mut input).unwrap();
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
= note: `-D clippy::read-line-without-trim` implied by `-D warnings`
15+
16+
error: calling `.parse()` without trimming the trailing newline character
17+
--> $DIR/read_line_without_trim.rs:18:20
18+
|
19+
LL | let _x = input.parse::<i32>().unwrap();
20+
| ----- ^^^^^^^^^^^^^^
21+
| |
22+
| help: try: `input.trim_end()`
23+
|
24+
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
25+
--> $DIR/read_line_without_trim.rs:17:5
26+
|
27+
LL | std::io::stdin().read_line(&mut input).unwrap();
28+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29+
30+
error: calling `.parse()` without trimming the trailing newline character
31+
--> $DIR/read_line_without_trim.rs:22:20
32+
|
33+
LL | let _x = input.parse::<u32>().unwrap();
34+
| ----- ^^^^^^^^^^^^^^
35+
| |
36+
| help: try: `input.trim_end()`
37+
|
38+
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
39+
--> $DIR/read_line_without_trim.rs:21:5
40+
|
41+
LL | std::io::stdin().read_line(&mut input).unwrap();
42+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
43+
44+
error: calling `.parse()` without trimming the trailing newline character
45+
--> $DIR/read_line_without_trim.rs:26:20
46+
|
47+
LL | let _x = input.parse::<f32>().unwrap();
48+
| ----- ^^^^^^^^^^^^^^
49+
| |
50+
| help: try: `input.trim_end()`
51+
|
52+
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
53+
--> $DIR/read_line_without_trim.rs:25:5
54+
|
55+
LL | std::io::stdin().read_line(&mut input).unwrap();
56+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
57+
58+
error: calling `.parse()` without trimming the trailing newline character
59+
--> $DIR/read_line_without_trim.rs:30:20
60+
|
61+
LL | let _x = input.parse::<bool>().unwrap();
62+
| ----- ^^^^^^^^^^^^^^^
63+
| |
64+
| help: try: `input.trim_end()`
65+
|
66+
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
67+
--> $DIR/read_line_without_trim.rs:29:5
68+
|
69+
LL | std::io::stdin().read_line(&mut input).unwrap();
70+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
71+
72+
error: aborting due to 5 previous errors
73+

0 commit comments

Comments
 (0)