Skip to content

Commit fb06081

Browse files
committed
Auto merge of rust-lang#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 rust-lang#10970 that added this lint, so this is kinda a followup PR ^^
2 parents d12b53e + fd85db3 commit fb06081

File tree

5 files changed

+140
-38
lines changed

5 files changed

+140
-38
lines changed

Diff for: clippy_lints/src/methods/mod.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -3420,11 +3420,12 @@ declare_clippy_lint! {
34203420
declare_clippy_lint! {
34213421
/// ### What it does
34223422
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
3423-
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
3424-
/// always fail because the string has a trailing newline in it.
3423+
/// into a string, then later attempting to use that string for an operation that will never
3424+
/// work for strings with a trailing newline character in it (e.g. parsing into a `i32`).
34253425
///
34263426
/// ### Why is this bad?
3427-
/// The `.parse()` call will always fail.
3427+
/// The operation will always fail at runtime no matter what the user enters, thus
3428+
/// making it a useless operation.
34283429
///
34293430
/// ### Example
34303431
/// ```rust,ignore

Diff for: clippy_lints/src/methods/read_line_without_trim.rs

+71-24
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ use clippy_utils::source::snippet;
55
use clippy_utils::ty::is_type_diagnostic_item;
66
use clippy_utils::visitors::for_each_local_use_after_expr;
77
use clippy_utils::{get_parent_expr, match_def_path};
8+
use rustc_ast::LitKind;
89
use rustc_errors::Applicability;
910
use rustc_hir::def::Res;
10-
use rustc_hir::{Expr, ExprKind, QPath};
11+
use rustc_hir::{BinOpKind, Expr, ExprKind, QPath};
1112
use rustc_lint::LateContext;
1213
use rustc_middle::ty::{self, Ty};
1314
use rustc_span::sym;
1415

1516
use super::READ_LINE_WITHOUT_TRIM;
1617

18+
fn expr_is_string_literal_without_trailing_newline(expr: &Expr<'_>) -> bool {
19+
if let ExprKind::Lit(lit) = expr.kind
20+
&& let LitKind::Str(sym, _) = lit.node
21+
{
22+
!sym.as_str().ends_with('\n')
23+
} else {
24+
false
25+
}
26+
}
27+
1728
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
1829
fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool {
1930
// 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<
2738
&& let Res::Local(local_id) = path.res
2839
{
2940
// 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.
41+
// now let's check if the first use of the string passed to `::read_line()`
42+
// is used for operations that will always fail (e.g. parsing "6\n" into a number)
3243
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(_, args) = parse_result_ty.kind()
39-
&& let Some(ok_ty) = args[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| {
44+
if let Some(parent) = get_parent_expr(cx, expr) {
45+
let data = if let ExprKind::MethodCall(segment, recv, args, span) = parent.kind {
46+
if segment.ident.name == sym!(parse)
47+
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
48+
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
49+
&& let ty::Adt(_, substs) = parse_result_ty.kind()
50+
&& let Some(ok_ty) = substs[0].as_type()
51+
&& parse_fails_on_trailing_newline(ok_ty)
52+
{
53+
// Called `s.parse::<T>()` where `T` is a type we know for certain will fail
54+
// if the input has a trailing newline
55+
Some((
56+
span,
57+
"calling `.parse()` on a string without trimming the trailing newline character",
58+
"checking",
59+
))
60+
} else if segment.ident.name == sym!(ends_with)
61+
&& recv.span == expr.span
62+
&& let [arg] = args
63+
&& expr_is_string_literal_without_trailing_newline(arg)
64+
{
65+
// Called `s.ends_with(<some string literal>)` where the argument is a string literal that does
66+
// not end with a newline, thus always evaluating to false
67+
Some((
68+
parent.span,
69+
"checking the end of a string without trimming the trailing newline character",
70+
"parsing",
71+
))
72+
} else {
73+
None
74+
}
75+
} else if let ExprKind::Binary(binop, left, right) = parent.kind
76+
&& let BinOpKind::Eq = binop.node
77+
&& (expr_is_string_literal_without_trailing_newline(left)
78+
|| expr_is_string_literal_without_trailing_newline(right))
79+
{
80+
// `s == <some string literal>` where the string literal does not end with a newline
81+
Some((
82+
parent.span,
83+
"comparing a string literal without trimming the trailing newline character",
84+
"comparison",
85+
))
86+
} else {
87+
None
88+
};
89+
90+
if let Some((primary_span, lint_message, operation)) = data {
91+
span_lint_and_then(cx, READ_LINE_WITHOUT_TRIM, primary_span, lint_message, |diag| {
92+
let local_snippet = snippet(cx, expr.span, "<expr>");
93+
4994
diag.span_note(
5095
call.span,
51-
"call to `.read_line()` here, \
52-
which leaves a trailing newline character in the buffer, \
53-
which in turn will cause `.parse()` to fail",
96+
format!(
97+
"call to `.read_line()` here, \
98+
which leaves a trailing newline character in the buffer, \
99+
which in turn will cause the {operation} to always fail"
100+
),
54101
);
55102

56103
diag.span_suggestion(
@@ -59,8 +106,8 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<
59106
format!("{local_snippet}.trim_end()"),
60107
Applicability::MachineApplicable,
61108
);
62-
},
63-
);
109+
});
110+
}
64111
}
65112

66113
// only consider the first use to prevent this scenario:

Diff for: tests/ui/read_line_without_trim.fixed

+13
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@ fn main() {
3131
std::io::stdin().read_line(&mut input).unwrap();
3232
// this is actually ok, so don't lint here
3333
let _x = input.parse::<String>().unwrap();
34+
35+
// comparing with string literals
36+
let mut input = String::new();
37+
std::io::stdin().read_line(&mut input).unwrap();
38+
if input.trim_end() == "foo" {
39+
println!("This will never ever execute!");
40+
}
41+
42+
let mut input = String::new();
43+
std::io::stdin().read_line(&mut input).unwrap();
44+
if input.trim_end().ends_with("foo") {
45+
println!("Neither will this");
46+
}
3447
}

Diff for: tests/ui/read_line_without_trim.rs

+13
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@ fn main() {
3131
std::io::stdin().read_line(&mut input).unwrap();
3232
// this is actually ok, so don't lint here
3333
let _x = input.parse::<String>().unwrap();
34+
35+
// comparing with string literals
36+
let mut input = String::new();
37+
std::io::stdin().read_line(&mut input).unwrap();
38+
if input == "foo" {
39+
println!("This will never ever execute!");
40+
}
41+
42+
let mut input = String::new();
43+
std::io::stdin().read_line(&mut input).unwrap();
44+
if input.ends_with("foo") {
45+
println!("Neither will this");
46+
}
3447
}

Diff for: tests/ui/read_line_without_trim.stderr

+39-11
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,102 @@
1-
error: calling `.parse()` without trimming the trailing newline character
1+
error: calling `.parse()` on a string without trimming the trailing newline character
22
--> tests/ui/read_line_without_trim.rs:12:25
33
|
44
LL | let _x: i32 = input.parse().unwrap();
55
| ----- ^^^^^^^
66
| |
77
| help: try: `input.trim_end()`
88
|
9-
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
9+
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
1010
--> tests/ui/read_line_without_trim.rs:11:5
1111
|
1212
LL | std::io::stdin().read_line(&mut input).unwrap();
1313
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1414
= note: `-D clippy::read-line-without-trim` implied by `-D warnings`
1515
= help: to override `-D warnings` add `#[allow(clippy::read_line_without_trim)]`
1616

17-
error: calling `.parse()` without trimming the trailing newline character
17+
error: calling `.parse()` on a string without trimming the trailing newline character
1818
--> tests/ui/read_line_without_trim.rs:16:20
1919
|
2020
LL | let _x = input.parse::<i32>().unwrap();
2121
| ----- ^^^^^^^^^^^^^^
2222
| |
2323
| help: try: `input.trim_end()`
2424
|
25-
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
25+
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
2626
--> tests/ui/read_line_without_trim.rs:15:5
2727
|
2828
LL | std::io::stdin().read_line(&mut input).unwrap();
2929
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3030

31-
error: calling `.parse()` without trimming the trailing newline character
31+
error: calling `.parse()` on a string without trimming the trailing newline character
3232
--> tests/ui/read_line_without_trim.rs:20:20
3333
|
3434
LL | let _x = input.parse::<u32>().unwrap();
3535
| ----- ^^^^^^^^^^^^^^
3636
| |
3737
| help: try: `input.trim_end()`
3838
|
39-
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
39+
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
4040
--> tests/ui/read_line_without_trim.rs:19:5
4141
|
4242
LL | std::io::stdin().read_line(&mut input).unwrap();
4343
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4444

45-
error: calling `.parse()` without trimming the trailing newline character
45+
error: calling `.parse()` on a string without trimming the trailing newline character
4646
--> tests/ui/read_line_without_trim.rs:24:20
4747
|
4848
LL | let _x = input.parse::<f32>().unwrap();
4949
| ----- ^^^^^^^^^^^^^^
5050
| |
5151
| help: try: `input.trim_end()`
5252
|
53-
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
53+
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
5454
--> tests/ui/read_line_without_trim.rs:23:5
5555
|
5656
LL | std::io::stdin().read_line(&mut input).unwrap();
5757
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5858

59-
error: calling `.parse()` without trimming the trailing newline character
59+
error: calling `.parse()` on a string without trimming the trailing newline character
6060
--> tests/ui/read_line_without_trim.rs:28:20
6161
|
6262
LL | let _x = input.parse::<bool>().unwrap();
6363
| ----- ^^^^^^^^^^^^^^^
6464
| |
6565
| help: try: `input.trim_end()`
6666
|
67-
note: call to `.read_line()` here, which leaves a trailing newline character in the buffer, which in turn will cause `.parse()` to fail
67+
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
6868
--> tests/ui/read_line_without_trim.rs:27:5
6969
|
7070
LL | std::io::stdin().read_line(&mut input).unwrap();
7171
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7272

73-
error: aborting due to 5 previous errors
73+
error: comparing a string literal without trimming the trailing newline character
74+
--> tests/ui/read_line_without_trim.rs:38:8
75+
|
76+
LL | if input == "foo" {
77+
| -----^^^^^^^^^
78+
| |
79+
| help: try: `input.trim_end()`
80+
|
81+
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
82+
--> tests/ui/read_line_without_trim.rs:37:5
83+
|
84+
LL | std::io::stdin().read_line(&mut input).unwrap();
85+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86+
87+
error: checking the end of a string without trimming the trailing newline character
88+
--> tests/ui/read_line_without_trim.rs:44:8
89+
|
90+
LL | if input.ends_with("foo") {
91+
| -----^^^^^^^^^^^^^^^^^
92+
| |
93+
| help: try: `input.trim_end()`
94+
|
95+
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
96+
--> tests/ui/read_line_without_trim.rs:43:5
97+
|
98+
LL | std::io::stdin().read_line(&mut input).unwrap();
99+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
100+
101+
error: aborting due to 7 previous errors
74102

0 commit comments

Comments
 (0)