Skip to content

Commit ea4fc58

Browse files
committed
restructure lint code, update description, more cases
1 parent 8279a00 commit ea4fc58

File tree

5 files changed

+119
-52
lines changed

5 files changed

+119
-52
lines changed

clippy_lints/src/methods/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,11 +3351,12 @@ declare_clippy_lint! {
33513351

33523352
declare_clippy_lint! {
33533353
/// Looks for calls to [`Stdin::read_line`] to read a line from the standard input
3354-
/// into a string, then later attempting to parse this string into a type without first trimming it, which will
3355-
/// always fail because the string has a trailing newline in it.
3354+
/// into a string, then later attempting to use that string for an operation that will never
3355+
/// work for strings with a trailing newline character in it (e.g. parsing into a `i32`).
33563356
///
33573357
/// ### Why is this bad?
3358-
/// The `.parse()` call will always fail.
3358+
/// The operation will always fail at runtime no matter what the user enters, thus
3359+
/// making it a useless operation.
33593360
///
33603361
/// ### Example
33613362
/// ```rust,ignore

clippy_lints/src/methods/read_line_without_trim.rs

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ use rustc_span::sym;
1616

1717
use super::READ_LINE_WITHOUT_TRIM;
1818

19+
fn expr_is_string_literal_without_trailing_newline(expr: &Expr<'_>) -> bool {
20+
if let ExprKind::Lit(lit) = expr.kind
21+
&& let LitKind::Str(sym, _) = lit.node
22+
{
23+
!sym.as_str().ends_with('\n')
24+
} else {
25+
false
26+
}
27+
}
28+
1929
/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
2030
fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool {
2131
// only allow a very limited set of types for now, for which we 100% know parsing will fail
@@ -29,56 +39,58 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<
2939
&& let Res::Local(local_id) = path.res
3040
{
3141
// We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
32-
// now let's check if the first use of the string passed to `::read_line()` is
33-
// parsed into a type that will always fail if it has a trailing newline.
42+
// now let's check if the first use of the string passed to `::read_line()`
43+
// is used for operations that will always fail (e.g. parsing "6\n" into a number)
3444
for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| {
35-
if let Some(parent) = get_parent_expr(cx, expr)
36-
{
37-
if 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 ty::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: std::borrow::Cow<'_, str> = 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");
45+
if let Some(parent) = get_parent_expr(cx, expr) {
5546

56-
diag.span_suggestion(
57-
expr.span,
58-
"try",
59-
format!("{local_snippet}.trim_end()"),
60-
Applicability::MachineApplicable,
61-
);
62-
}
63-
);
47+
let data = if let ExprKind::MethodCall(segment, recv, args, span) = parent.kind
48+
{
49+
if segment.ident.name == sym!(parse)
50+
&& let parse_result_ty = cx.typeck_results().expr_ty(parent)
51+
&& is_type_diagnostic_item(cx, parse_result_ty, sym::Result)
52+
&& let ty::Adt(_, substs) = parse_result_ty.kind()
53+
&& let Some(ok_ty) = substs[0].as_type()
54+
&& parse_fails_on_trailing_newline(ok_ty)
55+
{
56+
// Called `s.parse::<T>()` where `T` is a type we know for certain will fail
57+
// if the input has a trailing newline
58+
Some((span, "calling `.parse()` on a string without trimming the trailing newline character", "checking"))
59+
} else if segment.ident.name == sym!(ends_with)
60+
&& recv.span == expr.span
61+
&& let [arg] = args
62+
&& expr_is_string_literal_without_trailing_newline(arg)
63+
{
64+
// Called `s.ends_with(<some string literal>)` where the argument is a string literal that does
65+
// not end with a newline, thus always evaluating to false
66+
Some((parent.span, "checking the end of a string without trimming the trailing newline character", "parsing"))
67+
} else {
68+
None
69+
}
6470
} else if let ExprKind::Binary(binop, left, right) = parent.kind
6571
&& let BinOpKind::Eq = binop.node
66-
&& let ExprKind::Lit(lit) = right.kind
67-
&& let LitKind::Str(sym, _) = lit.node
68-
&& !sym.as_str().ends_with('\n')
72+
&& (expr_is_string_literal_without_trailing_newline(left)
73+
|| expr_is_string_literal_without_trailing_newline(right))
6974
{
75+
// `s == <some string literal>` where the string literal does not end with a newline
76+
Some((parent.span, "comparing a string literal without trimming the trailing newline character", "comparison"))
77+
} else {
78+
None
79+
};
80+
81+
if let Some((primary_span, lint_message, operation)) = data {
7082
span_lint_and_then(
7183
cx,
7284
READ_LINE_WITHOUT_TRIM,
73-
parent.span,
74-
"comparing a string literal without trimming the trailing newline character",
85+
primary_span,
86+
lint_message,
7587
|diag| {
7688
let local_snippet: std::borrow::Cow<'_, str> = snippet(cx, expr.span, "<expr>");
7789

78-
diag.span_note(call.span, "call to `.read_line()` here, \
90+
diag.span_note(call.span, format!("call to `.read_line()` here, \
7991
which leaves a trailing newline character in the buffer, \
80-
which in turn will cause the comparison to always fail");
81-
92+
which in turn will cause the {operation} to always fail"));
93+
8294
diag.span_suggestion(
8395
expr.span,
8496
"try",

tests/ui/read_line_without_trim.fixed

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

tests/ui/read_line_without_trim.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,17 @@ fn main() {
3333
std::io::stdin().read_line(&mut input).unwrap();
3434
// this is actually ok, so don't lint here
3535
let _x = input.parse::<String>().unwrap();
36+
37+
// comparing with string literals
38+
let mut input = String::new();
39+
std::io::stdin().read_line(&mut input).unwrap();
40+
if input == "foo" {
41+
println!("This will never ever execute!");
42+
}
43+
44+
let mut input = String::new();
45+
std::io::stdin().read_line(&mut input).unwrap();
46+
if input.ends_with("foo") {
47+
println!("Neither will this");
48+
}
3649
}
Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,101 @@
1-
error: calling `.parse()` without trimming the trailing newline character
1+
error: calling `.parse()` on a string without trimming the trailing newline character
22
--> $DIR/read_line_without_trim.rs:14: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
--> $DIR/read_line_without_trim.rs:13: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

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

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

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

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

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

0 commit comments

Comments
 (0)