mirror of
https://github.com/rust-lang/rust-clippy
synced 2024-11-30 16:39:26 +00:00
Auto merge of #11136 - y21:enhance_read_line_without_trim, r=dswij
[`read_line_without_trim`]: detect string literal comparison and `.ends_with()` calls This lint now also realizes that a comparison like `s == "foo"` and calls such as `s.ends_with("foo")` will fail if `s` was initialized by a call to `Stdin::read_line` (because of the trailing newline). changelog: [`read_line_without_trim`]: detect string literal comparison and `.ends_with()` calls r? `@giraffate` assigning you because you reviewed #10970 that added this lint, so this is kinda a followup PR ^^
This commit is contained in:
commit
fb060815b3
5 changed files with 140 additions and 38 deletions
|
@ -3420,11 +3420,12 @@ declare_clippy_lint! {
|
||||||
declare_clippy_lint! {
|
declare_clippy_lint! {
|
||||||
/// ### What it does
|
/// ### What it does
|
||||||
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
|
/// 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
|
/// into a string, then later attempting to use that string for an operation that will never
|
||||||
/// always fail because the string has a trailing newline in it.
|
/// work for strings with a trailing newline character in it (e.g. parsing into a `i32`).
|
||||||
///
|
///
|
||||||
/// ### Why is this bad?
|
/// ### Why is this bad?
|
||||||
/// The `.parse()` call will always fail.
|
/// The operation will always fail at runtime no matter what the user enters, thus
|
||||||
|
/// making it a useless operation.
|
||||||
///
|
///
|
||||||
/// ### Example
|
/// ### Example
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
|
|
|
@ -5,15 +5,26 @@ use clippy_utils::source::snippet;
|
||||||
use clippy_utils::ty::is_type_diagnostic_item;
|
use clippy_utils::ty::is_type_diagnostic_item;
|
||||||
use clippy_utils::visitors::for_each_local_use_after_expr;
|
use clippy_utils::visitors::for_each_local_use_after_expr;
|
||||||
use clippy_utils::{get_parent_expr, match_def_path};
|
use clippy_utils::{get_parent_expr, match_def_path};
|
||||||
|
use rustc_ast::LitKind;
|
||||||
use rustc_errors::Applicability;
|
use rustc_errors::Applicability;
|
||||||
use rustc_hir::def::Res;
|
use rustc_hir::def::Res;
|
||||||
use rustc_hir::{Expr, ExprKind, QPath};
|
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
|
||||||
use rustc_lint::LateContext;
|
use rustc_lint::LateContext;
|
||||||
use rustc_middle::ty::{self, Ty};
|
use rustc_middle::ty::{self, Ty};
|
||||||
use rustc_span::sym;
|
use rustc_span::sym;
|
||||||
|
|
||||||
use super::READ_LINE_WITHOUT_TRIM;
|
use super::READ_LINE_WITHOUT_TRIM;
|
||||||
|
|
||||||
|
fn expr_is_string_literal_without_trailing_newline(expr: &Expr<'_>) -> bool {
|
||||||
|
if let ExprKind::Lit(lit) = expr.kind
|
||||||
|
&& let LitKind::Str(sym, _) = lit.node
|
||||||
|
{
|
||||||
|
!sym.as_str().ends_with('\n')
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
|
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
|
||||||
fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool {
|
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
|
// only allow a very limited set of types for now, for which we 100% know parsing will fail
|
||||||
|
@ -27,30 +38,66 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<
|
||||||
&& let Res::Local(local_id) = path.res
|
&& let Res::Local(local_id) = path.res
|
||||||
{
|
{
|
||||||
// We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
|
// 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
|
// now let's check if the first use of the string passed to `::read_line()`
|
||||||
// parsed into a type that will always fail if it has a trailing newline.
|
// is used for operations that will always fail (e.g. parsing "6\n" into a number)
|
||||||
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
|
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
|
||||||
if let Some(parent) = get_parent_expr(cx, expr)
|
if let Some(parent) = get_parent_expr(cx, expr) {
|
||||||
&& let ExprKind::MethodCall(segment, .., span) = parent.kind
|
let data = if let ExprKind::MethodCall(segment, recv, args, span) = parent.kind {
|
||||||
&& segment.ident.name == sym!(parse)
|
if segment.ident.name == sym!(parse)
|
||||||
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
|
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
|
||||||
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
|
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
|
||||||
&& let ty::Adt(_, args) = parse_result_ty.kind()
|
&& let ty::Adt(_, substs) = parse_result_ty.kind()
|
||||||
&& let Some(ok_ty) = args[0].as_type()
|
&& let Some(ok_ty) = substs[0].as_type()
|
||||||
&& parse_fails_on_trailing_newline(ok_ty)
|
&& parse_fails_on_trailing_newline(ok_ty)
|
||||||
{
|
{
|
||||||
let local_snippet = snippet(cx, expr.span, "<expr>");
|
// Called `s.parse::<T>()` where `T` is a type we know for certain will fail
|
||||||
span_lint_and_then(
|
// if the input has a trailing newline
|
||||||
cx,
|
Some((
|
||||||
READ_LINE_WITHOUT_TRIM,
|
|
||||||
span,
|
span,
|
||||||
"calling `.parse()` without trimming the trailing newline character",
|
"calling `.parse()` on a string without trimming the trailing newline character",
|
||||||
|diag| {
|
"checking",
|
||||||
|
))
|
||||||
|
} else if segment.ident.name == sym!(ends_with)
|
||||||
|
&& recv.span == expr.span
|
||||||
|
&& let [arg] = args
|
||||||
|
&& expr_is_string_literal_without_trailing_newline(arg)
|
||||||
|
{
|
||||||
|
// Called `s.ends_with(<some string literal>)` where the argument is a string literal that does
|
||||||
|
// not end with a newline, thus always evaluating to false
|
||||||
|
Some((
|
||||||
|
parent.span,
|
||||||
|
"checking the end of a string without trimming the trailing newline character",
|
||||||
|
"parsing",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if let ExprKind::Binary(binop, left, right) = parent.kind
|
||||||
|
&& let BinOpKind::Eq = binop.node
|
||||||
|
&& (expr_is_string_literal_without_trailing_newline(left)
|
||||||
|
|| expr_is_string_literal_without_trailing_newline(right))
|
||||||
|
{
|
||||||
|
// `s == <some string literal>` where the string literal does not end with a newline
|
||||||
|
Some((
|
||||||
|
parent.span,
|
||||||
|
"comparing a string literal without trimming the trailing newline character",
|
||||||
|
"comparison",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((primary_span, lint_message, operation)) = data {
|
||||||
|
span_lint_and_then(cx, READ_LINE_WITHOUT_TRIM, primary_span, lint_message, |diag| {
|
||||||
|
let local_snippet = snippet(cx, expr.span, "<expr>");
|
||||||
|
|
||||||
diag.span_note(
|
diag.span_note(
|
||||||
call.span,
|
call.span,
|
||||||
|
format!(
|
||||||
"call to `.read_line()` here, \
|
"call to `.read_line()` here, \
|
||||||
which leaves a trailing newline character in the buffer, \
|
which leaves a trailing newline character in the buffer, \
|
||||||
which in turn will cause `.parse()` to fail",
|
which in turn will cause the {operation} to always fail"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
diag.span_suggestion(
|
diag.span_suggestion(
|
||||||
|
@ -59,8 +106,8 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<
|
||||||
format!("{local_snippet}.trim_end()"),
|
format!("{local_snippet}.trim_end()"),
|
||||||
Applicability::MachineApplicable,
|
Applicability::MachineApplicable,
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only consider the first use to prevent this scenario:
|
// only consider the first use to prevent this scenario:
|
||||||
|
|
|
@ -31,4 +31,17 @@ fn main() {
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
// this is actually ok, so don't lint here
|
// this is actually ok, so don't lint here
|
||||||
let _x = input.parse::<String>().unwrap();
|
let _x = input.parse::<String>().unwrap();
|
||||||
|
|
||||||
|
// comparing with string literals
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
if input.trim_end() == "foo" {
|
||||||
|
println!("This will never ever execute!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
if input.trim_end().ends_with("foo") {
|
||||||
|
println!("Neither will this");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,4 +31,17 @@ fn main() {
|
||||||
std::io::stdin().read_line(&mut input).unwrap();
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
// this is actually ok, so don't lint here
|
// this is actually ok, so don't lint here
|
||||||
let _x = input.parse::<String>().unwrap();
|
let _x = input.parse::<String>().unwrap();
|
||||||
|
|
||||||
|
// comparing with string literals
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
if input == "foo" {
|
||||||
|
println!("This will never ever execute!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
if input.ends_with("foo") {
|
||||||
|
println!("Neither will this");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
error: calling `.parse()` without trimming the trailing newline character
|
error: calling `.parse()` on a string without trimming the trailing newline character
|
||||||
--> tests/ui/read_line_without_trim.rs:12:25
|
--> tests/ui/read_line_without_trim.rs:12:25
|
||||||
|
|
|
|
||||||
LL | let _x: i32 = input.parse().unwrap();
|
LL | let _x: i32 = input.parse().unwrap();
|
||||||
|
@ -6,7 +6,7 @@ LL | let _x: i32 = input.parse().unwrap();
|
||||||
| |
|
| |
|
||||||
| help: try: `input.trim_end()`
|
| 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
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause the checking to always fail
|
||||||
--> tests/ui/read_line_without_trim.rs:11:5
|
--> tests/ui/read_line_without_trim.rs:11:5
|
||||||
|
|
|
|
||||||
LL | std::io::stdin().read_line(&mut input).unwrap();
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
@ -14,7 +14,7 @@ LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
= note: `-D clippy::read-line-without-trim` implied by `-D warnings`
|
= note: `-D clippy::read-line-without-trim` implied by `-D warnings`
|
||||||
= help: to override `-D warnings` add `#[allow(clippy::read_line_without_trim)]`
|
= help: to override `-D warnings` add `#[allow(clippy::read_line_without_trim)]`
|
||||||
|
|
||||||
error: calling `.parse()` without trimming the trailing newline character
|
error: calling `.parse()` on a string without trimming the trailing newline character
|
||||||
--> tests/ui/read_line_without_trim.rs:16:20
|
--> tests/ui/read_line_without_trim.rs:16:20
|
||||||
|
|
|
|
||||||
LL | let _x = input.parse::<i32>().unwrap();
|
LL | let _x = input.parse::<i32>().unwrap();
|
||||||
|
@ -22,13 +22,13 @@ LL | let _x = input.parse::<i32>().unwrap();
|
||||||
| |
|
| |
|
||||||
| help: try: `input.trim_end()`
|
| 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
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause the checking to always fail
|
||||||
--> tests/ui/read_line_without_trim.rs:15:5
|
--> tests/ui/read_line_without_trim.rs:15:5
|
||||||
|
|
|
|
||||||
LL | std::io::stdin().read_line(&mut input).unwrap();
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: calling `.parse()` without trimming the trailing newline character
|
error: calling `.parse()` on a string without trimming the trailing newline character
|
||||||
--> tests/ui/read_line_without_trim.rs:20:20
|
--> tests/ui/read_line_without_trim.rs:20:20
|
||||||
|
|
|
|
||||||
LL | let _x = input.parse::<u32>().unwrap();
|
LL | let _x = input.parse::<u32>().unwrap();
|
||||||
|
@ -36,13 +36,13 @@ LL | let _x = input.parse::<u32>().unwrap();
|
||||||
| |
|
| |
|
||||||
| help: try: `input.trim_end()`
|
| 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
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause the checking to always fail
|
||||||
--> tests/ui/read_line_without_trim.rs:19:5
|
--> tests/ui/read_line_without_trim.rs:19:5
|
||||||
|
|
|
|
||||||
LL | std::io::stdin().read_line(&mut input).unwrap();
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: calling `.parse()` without trimming the trailing newline character
|
error: calling `.parse()` on a string without trimming the trailing newline character
|
||||||
--> tests/ui/read_line_without_trim.rs:24:20
|
--> tests/ui/read_line_without_trim.rs:24:20
|
||||||
|
|
|
|
||||||
LL | let _x = input.parse::<f32>().unwrap();
|
LL | let _x = input.parse::<f32>().unwrap();
|
||||||
|
@ -50,13 +50,13 @@ LL | let _x = input.parse::<f32>().unwrap();
|
||||||
| |
|
| |
|
||||||
| help: try: `input.trim_end()`
|
| 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
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause the checking to always fail
|
||||||
--> tests/ui/read_line_without_trim.rs:23:5
|
--> tests/ui/read_line_without_trim.rs:23:5
|
||||||
|
|
|
|
||||||
LL | std::io::stdin().read_line(&mut input).unwrap();
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: calling `.parse()` without trimming the trailing newline character
|
error: calling `.parse()` on a string without trimming the trailing newline character
|
||||||
--> tests/ui/read_line_without_trim.rs:28:20
|
--> tests/ui/read_line_without_trim.rs:28:20
|
||||||
|
|
|
|
||||||
LL | let _x = input.parse::<bool>().unwrap();
|
LL | let _x = input.parse::<bool>().unwrap();
|
||||||
|
@ -64,11 +64,39 @@ LL | let _x = input.parse::<bool>().unwrap();
|
||||||
| |
|
| |
|
||||||
| help: try: `input.trim_end()`
|
| 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
|
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause the checking to always fail
|
||||||
--> tests/ui/read_line_without_trim.rs:27:5
|
--> tests/ui/read_line_without_trim.rs:27:5
|
||||||
|
|
|
|
||||||
LL | std::io::stdin().read_line(&mut input).unwrap();
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
error: aborting due to 5 previous errors
|
error: comparing a string literal without trimming the trailing newline character
|
||||||
|
--> tests/ui/read_line_without_trim.rs:38:8
|
||||||
|
|
|
||||||
|
LL | if input == "foo" {
|
||||||
|
| -----^^^^^^^^^
|
||||||
|
| |
|
||||||
|
| 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 the comparison to always fail
|
||||||
|
--> tests/ui/read_line_without_trim.rs:37:5
|
||||||
|
|
|
||||||
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: checking the end of a string without trimming the trailing newline character
|
||||||
|
--> tests/ui/read_line_without_trim.rs:44:8
|
||||||
|
|
|
||||||
|
LL | if input.ends_with("foo") {
|
||||||
|
| -----^^^^^^^^^^^^^^^^^
|
||||||
|
| |
|
||||||
|
| 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 the parsing to always fail
|
||||||
|
--> tests/ui/read_line_without_trim.rs:43:5
|
||||||
|
|
|
||||||
|
LL | std::io::stdin().read_line(&mut input).unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
error: aborting due to 7 previous errors
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue