@@ -15,6 +15,16 @@ use rustc_span::sym;
1515
1616use super :: READ_LINE_WITHOUT_TRIM ;
1717
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+
1828/// Will a `.parse::<ty>()` call fail if the input has a trailing newline?
1929fn parse_fails_on_trailing_newline ( ty : Ty < ' _ > ) -> bool {
2030 // only allow a very limited set of types for now, for which we 100% know parsing will fail
@@ -28,69 +38,75 @@ pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<
2838 && let Res :: Local ( local_id) = path. res
2939 {
3040 // We've checked that `call` is a call to `Stdin::read_line()` with the right receiver,
31- // now let's check if the first use of the string passed to `::read_line()` is
32- // 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)
3343 for_each_local_use_after_expr ( cx, local_id, call. hir_id , |expr| {
3444 if let Some ( parent) = get_parent_expr ( cx, expr) {
35- if let ExprKind :: MethodCall ( segment, .. , span) = parent. kind
36- && segment. ident . name == sym ! ( parse)
37- && let parse_result_ty = cx. typeck_results ( ) . expr_ty ( parent)
38- && is_type_diagnostic_item ( cx, parse_result_ty, sym:: Result )
39- && let ty:: Adt ( _, substs) = parse_result_ty. kind ( )
40- && let Some ( ok_ty) = substs[ 0 ] . as_type ( )
41- && parse_fails_on_trailing_newline ( ok_ty)
42- {
43- let local_snippet : std :: borrow :: Cow < ' _ , str > = snippet ( cx , expr . span , "<expr>" ) ;
44- span_lint_and_then (
45- cx ,
46- READ_LINE_WITHOUT_TRIM ,
47- span ,
48- "calling `.parse()` without trimming the trailing newline character ",
49- |diag| {
50- diag . span_note (
51- call . span ,
52- "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-
57- diag . span_suggestion (
58- expr . span ,
59- "try ",
60- format ! ( "{local_snippet}.trim_end()" ) ,
61- Applicability :: MachineApplicable ,
62- ) ;
63- } ,
64- ) ;
65- } else if let ExprKind :: Binary ( binop, _ , right) = parent. kind
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
6676 && let BinOpKind :: Eq = binop. node
67- && let ExprKind :: Lit ( lit) = right. kind
68- && let LitKind :: Str ( sym, _) = lit. node
69- && !sym. as_str ( ) . ends_with ( '\n' )
77+ && ( expr_is_string_literal_without_trailing_newline ( left)
78+ || expr_is_string_literal_without_trailing_newline ( right) )
7079 {
71- span_lint_and_then (
72- cx,
73- READ_LINE_WITHOUT_TRIM ,
80+ // `s == <some string literal>` where the string literal does not end with a newline
81+ Some ( (
7482 parent. span ,
7583 "comparing a string literal without trimming the trailing newline character" ,
76- |diag| {
77- let local_snippet = snippet ( cx, expr. span , "<expr>" ) ;
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>" ) ;
7893
79- diag. span_note (
80- call. span ,
94+ diag. span_note (
95+ call. span ,
96+ format ! (
8197 "call to `.read_line()` here, \
8298 which leaves a trailing newline character in the buffer, \
83- which in turn will cause the comparison to always fail",
84- ) ;
99+ which in turn will cause the {operation} to always fail"
100+ ) ,
101+ ) ;
85102
86- diag. span_suggestion (
87- expr. span ,
88- "try" ,
89- format ! ( "{local_snippet}.trim_end()" ) ,
90- Applicability :: MachineApplicable ,
91- ) ;
92- } ,
93- ) ;
103+ diag. span_suggestion (
104+ expr. span ,
105+ "try" ,
106+ format ! ( "{local_snippet}.trim_end()" ) ,
107+ Applicability :: MachineApplicable ,
108+ ) ;
109+ } ) ;
94110 }
95111 }
96112
0 commit comments