new lint: `read_line_without_trim`

This commit is contained in:
y21 2023-06-17 01:41:11 +02:00
parent ba3bd8f0f1
commit 8fad54e8f9
7 changed files with 179 additions and 0 deletions

View File

@ -5134,6 +5134,7 @@ Released 2018-09-13
[`rc_buffer`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_buffer
[`rc_clone_in_vec_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_clone_in_vec_init
[`rc_mutex`]: https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex
[`read_line_without_trim`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_line_without_trim
[`read_zero_byte_vec`]: https://rust-lang.github.io/rust-clippy/master/index.html#read_zero_byte_vec
[`recursive_format_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#recursive_format_impl
[`redundant_allocation`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_allocation

View File

@ -389,6 +389,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::methods::OR_THEN_UNWRAP_INFO,
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
crate::methods::RANGE_ZIP_WITH_LEN_INFO,
crate::methods::READ_LINE_WITHOUT_TRIM_INFO,
crate::methods::REPEAT_ONCE_INFO,
crate::methods::RESULT_MAP_OR_INTO_OPTION_INFO,
crate::methods::SEARCH_IS_SOME_INFO,

View File

@ -72,6 +72,7 @@ mod or_fun_call;
mod or_then_unwrap;
mod path_buf_push_overwrite;
mod range_zip_with_len;
mod read_line_without_trim;
mod repeat_once;
mod search_is_some;
mod seek_from_current;
@ -3348,6 +3349,35 @@ declare_clippy_lint! {
"checks for usage of `Iterator::fold` with a type that implements `Try`"
}
declare_clippy_lint! {
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
/// always fail because the string has a trailing newline in it.
///
/// ### Why is this bad?
/// The `.parse()` call will always fail.
///
/// ### Example
/// ```rust,ignore
/// let mut input = String::new();
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
/// let num: i32 = input.parse().expect("Not a number!");
/// assert_eq!(num, 42); // we never even get here!
/// ```
/// Use instead:
/// ```rust,ignore
/// let mut input = String::new();
/// std::io::stdin().read_line(&mut input).expect("Failed to read a line");
/// let num: i32 = input.trim_end().parse().expect("Not a number!");
/// // ^^^^^^^^^^^ remove the trailing newline
/// assert_eq!(num, 42);
/// ```
#[clippy::version = "1.72.0"]
pub READ_LINE_WITHOUT_TRIM,
correctness,
"calling `Stdin::read_line`, then trying to parse it without first trimming"
}
pub struct Methods {
avoid_breaking_exported_api: bool,
msrv: Msrv,
@ -3468,6 +3498,7 @@ impl_lint_pass!(Methods => [
REPEAT_ONCE,
STABLE_SORT_PRIMITIVE,
UNIT_HASH,
READ_LINE_WITHOUT_TRIM,
UNNECESSARY_SORT_BY,
VEC_RESIZE_TO_ZERO,
VERBOSE_FILE_READS,
@ -3879,6 +3910,9 @@ impl Methods {
("read_to_string", [_]) => {
verbose_file_reads::check(cx, expr, recv, verbose_file_reads::READ_TO_STRING_MSG);
},
("read_line", [arg]) => {
read_line_without_trim::check(cx, expr, recv, arg);
}
("repeat", [arg]) => {
repeat_once::check(cx, expr, recv, arg);
},

View File

@ -0,0 +1,74 @@
use std::ops::ControlFlow;
use clippy_utils::{
diagnostics::span_lint_and_then, get_parent_expr, match_def_path, source::snippet, ty::is_type_diagnostic_item,
visitors::for_each_local_use_after_expr,
};
use rustc_errors::Applicability;
use rustc_hir::Expr;
use rustc_hir::QPath;
use rustc_hir::{def::Res, ExprKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, Ty};
use rustc_span::sym;
use super::READ_LINE_WITHOUT_TRIM;
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool {
// only allow a very limited set of types for now, for which we 100% know parsing will fail
matches!(ty.kind(), ty::Float(_) | ty::Bool | ty::Int(_) | ty::Uint(_))
}
pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) {
if let Some(recv_adt) = cx.typeck_results().expr_ty(recv).ty_adt_def()
&& match_def_path(cx, recv_adt.did(), &["std", "io", "stdio", "Stdin"])
&& let ExprKind::Path(QPath::Resolved(_, path)) = arg.peel_borrows().kind
&& let Res::Local(local_id) = path.res
{
// We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
// now let's check if the first use of the string passed to `::read_line()` is
// parsed into a type that will always fail if it has a trailing newline.
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
if let Some(parent) = get_parent_expr(cx, expr)
&& let ExprKind::MethodCall(segment, .., span) = parent.kind
&& segment.ident.name == sym!(parse)
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
&& let ty::Adt(_, substs) = parse_result_ty.kind()
&& let Some(ok_ty) = substs[0].as_type()
&& parse_fails_on_trailing_newline(ok_ty)
{
let local_snippet = snippet(cx, expr.span, "<expr>");
span_lint_and_then(
cx,
READ_LINE_WITHOUT_TRIM,
span,
"calling `.parse()` without trimming the trailing newline character",
|diag| {
diag.span_note(call.span, "call to `.read_line()` here, \
which leaves a trailing newline character in the buffer, \
which in turn will cause `.parse()` to fail");
diag.span_suggestion(
expr.span,
"try",
format!("{local_snippet}.trim_end()"),
Applicability::MachineApplicable,
);
}
);
}
// only consider the first use to prevent this scenario:
// ```
// let mut s = String::new();
// std::io::stdin().read_line(&mut s);
// s.pop();
// let _x: i32 = s.parse().unwrap();
// ```
// this is actually fine, because the pop call removes the trailing newline.
ControlFlow::<(), ()>::Break(())
});
}
}

View File

@ -0,0 +1,19 @@
//@run-rustfix
#![allow(unused)]
#![warn(clippy::read_line_without_trim)]
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
input.pop();
let _x: i32 = input.parse().unwrap(); // don't trigger here, newline character is popped
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let _x: i32 = input.trim_end().parse().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let _x = input.trim_end().parse::<i32>().unwrap();
}

View File

@ -0,0 +1,19 @@
//@run-rustfix
#![allow(unused)]
#![warn(clippy::read_line_without_trim)]
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
input.pop();
let _x: i32 = input.parse().unwrap(); // don't trigger here, newline character is popped
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let _x: i32 = input.parse().unwrap();
let mut input = String::new();
std::io::stdin().read_line(&mut input).unwrap();
let _x = input.parse::<i32>().unwrap();
}

View File

@ -0,0 +1,31 @@
error: calling `.parse()` without trimming the trailing newline character
--> $DIR/read_line_without_trim.rs:14:25
|
LL | let _x: i32 = input.parse().unwrap();
| ----- ^^^^^^^
| |
| help: try: `input.trim_end()`
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
--> $DIR/read_line_without_trim.rs:13:5
|
LL | std::io::stdin().read_line(&mut input).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: `-D clippy::read-line-without-trim` implied by `-D warnings`
error: calling `.parse()` without trimming the trailing newline character
--> $DIR/read_line_without_trim.rs:18:20
|
LL | let _x = input.parse::<i32>().unwrap();
| ----- ^^^^^^^^^^^^^^
| |
| help: try: `input.trim_end()`
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
--> $DIR/read_line_without_trim.rs:17:5
|
LL | std::io::stdin().read_line(&mut input).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 2 previous errors