|
| 1 | +use std::ops::ControlFlow; |
| 2 | + |
| 3 | +use clippy_utils::{ |
| 4 | + diagnostics::span_lint_and_then, get_parent_expr, match_def_path, source::snippet, ty::is_type_diagnostic_item, |
| 5 | + visitors::for_each_local_use_after_expr, |
| 6 | +}; |
| 7 | +use rustc_errors::Applicability; |
| 8 | +use rustc_hir::Expr; |
| 9 | +use rustc_hir::QPath; |
| 10 | +use rustc_hir::{def::Res, ExprKind}; |
| 11 | +use rustc_lint::LateContext; |
| 12 | +use rustc_middle::ty::{self, Ty}; |
| 13 | +use rustc_span::sym; |
| 14 | + |
| 15 | +use super::READ_LINE_WITHOUT_TRIM; |
| 16 | + |
| 17 | +/// Will a `.parse::<ty>()` call fail if the input has a trailing newline? |
| 18 | +fn parse_fails_on_trailing_newline(ty: Ty<'_>) -> bool { |
| 19 | + // only allow a very limited set of types for now, for which we 100% know parsing will fail |
| 20 | + matches!(ty.kind(), ty::Float(_) | ty::Bool | ty::Int(_) | ty::Uint(_)) |
| 21 | +} |
| 22 | + |
| 23 | +pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) { |
| 24 | + if let Some(recv_adt) = cx.typeck_results().expr_ty(recv).ty_adt_def() |
| 25 | + && match_def_path(cx, recv_adt.did(), &["std", "io", "stdio", "Stdin"]) |
| 26 | + && let ExprKind::Path(QPath::Resolved(_, path)) = arg.peel_borrows().kind |
| 27 | + && let Res::Local(local_id) = path.res |
| 28 | + { |
| 29 | + // 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. |
| 32 | + 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(_, substs) = parse_result_ty.kind() |
| 39 | + && let Some(ok_ty) = substs[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| { |
| 49 | + diag.span_note(call.span, "call to `.read_line()` here, \ |
| 50 | + which leaves a trailing newline character in the buffer, \ |
| 51 | + which in turn will cause `.parse()` to fail"); |
| 52 | + |
| 53 | + diag.span_suggestion( |
| 54 | + expr.span, |
| 55 | + "try", |
| 56 | + format!("{local_snippet}.trim_end()"), |
| 57 | + Applicability::MachineApplicable, |
| 58 | + ); |
| 59 | + } |
| 60 | + ); |
| 61 | + } |
| 62 | + |
| 63 | + // only consider the first use to prevent this scenario: |
| 64 | + // ``` |
| 65 | + // let mut s = String::new(); |
| 66 | + // std::io::stdin().read_line(&mut s); |
| 67 | + // s.pop(); |
| 68 | + // let _x: i32 = s.parse().unwrap(); |
| 69 | + // ``` |
| 70 | + // this is actually fine, because the pop call removes the trailing newline. |
| 71 | + ControlFlow::<(), ()>::Break(()) |
| 72 | + }); |
| 73 | + } |
| 74 | +} |
0 commit comments