Skip to content

Commit 5662d71

Browse files
committed
new lint: read_line_without_trim
1 parent 3217f8a commit 5662d71

File tree

7 files changed

+182
-0
lines changed

7 files changed

+182
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5101,6 +5101,7 @@ Released 2018-09-13
51015101
[`rc_buffer`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer
51025102
[`rc_clone_in_vec_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_clone_in_vec_init
51035103
[`rc_mutex`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex
5104+
[`read_line_without_trim`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_line_without_trim
51045105
[`read_zero_byte_vec`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_zero_byte_vec
51055106
[`recursive_format_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#recursive_format_impl
51065107
[`redundant_allocation`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_allocation

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
388388
crate::methods::OR_THEN_UNWRAP_INFO,
389389
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
390390
crate::methods::RANGE_ZIP_WITH_LEN_INFO,
391+
crate::methods::READ_LINE_WITHOUT_TRIM_INFO,
391392
crate::methods::REPEAT_ONCE_INFO,
392393
crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO,
393394
crate::methods::SEARCH_IS_SOME_INFO,

clippy_lints/src/methods/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ mod or_fun_call;
7171
mod or_then_unwrap;
7272
mod path_buf_push_overwrite;
7373
mod range_zip_with_len;
74+
mod read_line_without_trim;
7475
mod repeat_once;
7576
mod search_is_some;
7677
mod seek_from_current;
@@ -3284,6 +3285,35 @@ declare_clippy_lint! {
32843285
"calling `.drain(..).collect()` to move all elements into a new collection"
32853286
}
32863287

3288+
declare_clippy_lint! {
3289+
/// ### What it does
3290+
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
3291+
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
3292+
/// always fail because the string has a trailing newline in it.
3293+
///
3294+
/// ### Why is this bad?
3295+
/// The `.parse()` call will always fail.
3296+
///
3297+
/// ### Example
3298+
/// ```rust
3299+
/// let mut input = String::new();
3300+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3301+
/// let num: i32 = input.parse().expect("Not a number!");
3302+
/// assert_eq!(num, 42); // we never even get here!
3303+
/// ```
3304+
/// Use instead:
3305+
/// ```rust
3306+
/// let mut input = String::new();
3307+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3308+
/// let num: i32 = input.trim_end().parse().expect("Not a number!");
3309+
/// // ^^^^^^^^^^^ remove the trailing newline
3310+
/// assert_eq!(num, 42);
3311+
/// ```
3312+
#[clippy::version = "1.72.0"]
3313+
pub READ_LINE_WITHOUT_TRIM,
3314+
correctness,
3315+
"calling `Stdin::read_line`, then trying to parse it without first trimming"
3316+
}
32873317
pub struct Methods {
32883318
avoid_breaking_exported_api: bool,
32893319
msrv: Msrv,
@@ -3403,6 +3433,7 @@ impl_lint_pass!(Methods => [
34033433
REPEAT_ONCE,
34043434
STABLE_SORT_PRIMITIVE,
34053435
UNIT_HASH,
3436+
READ_LINE_WITHOUT_TRIM,
34063437
UNNECESSARY_SORT_BY,
34073438
VEC_RESIZE_TO_ZERO,
34083439
VERBOSE_FILE_READS,
@@ -3810,6 +3841,9 @@ impl Methods {
38103841
("read_to_string", [_]) => {
38113842
verbose_file_reads::check(cx, expr, recv, verbose_file_reads::READ_TO_STRING_MSG);
38123843
},
3844+
("read_line", [arg]) => {
3845+
read_line_without_trim::check(cx, expr, recv, arg);
3846+
}
38133847
("repeat", [arg]) => {
38143848
repeat_once::check(cx, expr, recv, arg);
38153849
},
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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::{Ty, TyKind};
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!(
21+
ty.kind(),
22+
TyKind::Float(_) | TyKind::Bool | TyKind::Int(_) | TyKind::Uint(_)
23+
)
24+
}
25+
26+
pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) {
27+
if let Some(recv_adt) = cx.typeck_results().expr_ty(recv).ty_adt_def()
28+
&& match_def_path(cx, recv_adt.did(), &["std", "io", "stdio", "Stdin"])
29+
&& let ExprKind::Path(QPath::Resolved(_, path)) = arg.peel_borrows().kind
30+
&& let Res::Local(local_id) = path.res
31+
{
32+
// We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
33+
// now let's check if the first use of the string passed to `::read_line()` is
34+
// parsed into a type that will always fail if it has a trailing newline.
35+
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
36+
if let Some(parent) = get_parent_expr(cx, expr)
37+
&& let ExprKind::MethodCall(segment, .., span) = parent.kind
38+
&& segment.ident.name == sym!(parse)
39+
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
40+
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
41+
&& let TyKind::Adt(_, substs) = parse_result_ty.kind()
42+
&& let Some(ok_ty) = substs[0].as_type()
43+
&& parse_fails_on_trailing_newline(ok_ty)
44+
{
45+
let local_snippet = snippet(cx, expr.span, "<expr>");
46+
span_lint_and_then(
47+
cx,
48+
READ_LINE_WITHOUT_TRIM,
49+
span,
50+
"calling `.parse()` without trimming the trailing newline character",
51+
|diag| {
52+
diag.span_note(call.span, "call to `.read_line()` here, \
53+
which leaves a trailing newline character in the buffer, \
54+
which in turn will cause `.parse()` to fail");
55+
56+
diag.span_suggestion(
57+
expr.span,
58+
"try",
59+
format!("{local_snippet}.trim_end()"),
60+
Applicability::MachineApplicable,
61+
);
62+
}
63+
);
64+
}
65+
66+
// only consider the first use to prevent this scenario:
67+
// ```
68+
// let mut s = String::new();
69+
// std::io::stdin().read_line(&mut s);
70+
// s.pop();
71+
// let _x: i32 = s.parse().unwrap();
72+
// ```
73+
// this is actually fine, because the pop call removes the trailing newline.
74+
ControlFlow::<(), ()>::Break(())
75+
});
76+
}
77+
}

tests/ui/read_line_without_trim.fixed

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
}

tests/ui/read_line_without_trim.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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: aborting due to 2 previous errors
31+

0 commit comments

Comments
 (0)