Skip to content

Commit 0fcca44

Browse files
committed
new lint: read_line_without_trim
1 parent 17a48c2 commit 0fcca44

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
@@ -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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 34 additions & 0 deletions
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;
@@ -3316,6 +3317,35 @@ declare_clippy_lint! {
33163317
"checks for usage of `Iterator::fold` with a type that implements `Try`"
33173318
}
33183319

3320+
declare_clippy_lint! {
3321+
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
3322+
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
3323+
/// always fail because the string has a trailing newline in it.
3324+
///
3325+
/// ### Why is this bad?
3326+
/// The `.parse()` call will always fail.
3327+
///
3328+
/// ### Example
3329+
/// ```rust
3330+
/// let mut input = String::new();
3331+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3332+
/// let num: i32 = input.parse().expect("Not a number!");
3333+
/// assert_eq!(num, 42); // we never even get here!
3334+
/// ```
3335+
/// Use instead:
3336+
/// ```rust
3337+
/// let mut input = String::new();
3338+
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
3339+
/// let num: i32 = input.trim_end().parse().expect("Not a number!");
3340+
/// // ^^^^^^^^^^^ remove the trailing newline
3341+
/// assert_eq!(num, 42);
3342+
/// ```
3343+
#[clippy::version = "1.72.0"]
3344+
pub READ_LINE_WITHOUT_TRIM,
3345+
correctness,
3346+
"calling `Stdin::read_line`, then trying to parse it without first trimming"
3347+
}
3348+
33193349
pub struct Methods {
33203350
avoid_breaking_exported_api: bool,
33213351
msrv: Msrv,
@@ -3435,6 +3465,7 @@ impl_lint_pass!(Methods => [
34353465
REPEAT_ONCE,
34363466
STABLE_SORT_PRIMITIVE,
34373467
UNIT_HASH,
3468+
READ_LINE_WITHOUT_TRIM,
34383469
UNNECESSARY_SORT_BY,
34393470
VEC_RESIZE_TO_ZERO,
34403471
VERBOSE_FILE_READS,
@@ -3846,6 +3877,9 @@ impl Methods {
38463877
("read_to_string", [_]) => {
38473878
verbose_file_reads::check(cx, expr, recv, verbose_file_reads::READ_TO_STRING_MSG);
38483879
},
3880+
("read_line", [arg]) => {
3881+
read_line_without_trim::check(cx, expr, recv, arg);
3882+
}
38493883
("repeat", [arg]) => {
38503884
repeat_once::check(cx, expr, recv, arg);
38513885
},
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)