use std::ops::ControlFlow; use clippy_utils::diagnostics::span_lint_and_then; use clippy_utils::source::snippet; use clippy_utils::ty::is_type_diagnostic_item; use clippy_utils::visitors::for_each_local_use_after_expr; use clippy_utils::{get_parent_expr, match_def_path}; use rustc_errors::Applicability; use rustc_hir::def::Res; use rustc_hir::{Expr, ExprKind, QPath}; use rustc_lint::LateContext; use rustc_middle::ty::{self, Ty}; use rustc_span::sym; use super::READ_LINE_WITHOUT_TRIM; /// Will a `.parse::()` call fail if the input has a trailing newline? 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 matches!(ty.kind(), ty::Float(_) | ty::Bool | ty::Int(_) | ty::Uint(_)) } pub fn check(cx: &LateContext<'_>, call: &Expr<'_>, recv: &Expr<'_>, arg: &Expr<'_>) { if let Some(recv_adt) = cx.typeck_results().expr_ty(recv).ty_adt_def() && match_def_path(cx, recv_adt.did(), &["std", "io", "stdio", "Stdin"]) && let ExprKind::Path(QPath::Resolved(_, path)) = arg.peel_borrows().kind && let Res::Local(local_id) = path.res { // 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 // parsed into a type that will always fail if it has a trailing newline. for_each_local_use_after_expr(cx, local_id, call.hir_id, |expr| { if let Some(parent) = get_parent_expr(cx, expr) && let ExprKind::MethodCall(segment, .., span) = parent.kind && segment.ident.name == sym!(parse) && let parse_result_ty = cx.typeck_results().expr_ty(parent) && is_type_diagnostic_item(cx, parse_result_ty, sym::Result) && let ty::Adt(_, args) = parse_result_ty.kind() && let Some(ok_ty) = args[0].as_type() && parse_fails_on_trailing_newline(ok_ty) { let local_snippet = snippet(cx, expr.span, ""); span_lint_and_then( cx, READ_LINE_WITHOUT_TRIM, span, "calling `.parse()` without trimming the trailing newline character", |diag| { diag.span_note( call.span, "call to `.read_line()` here, \ which leaves a trailing newline character in the buffer, \ which in turn will cause `.parse()` to fail", ); diag.span_suggestion( expr.span, "try", format!("{local_snippet}.trim_end()"), Applicability::MachineApplicable, ); }, ); } // only consider the first use to prevent this scenario: // ``` // let mut s = String::new(); // std::io::stdin().read_line(&mut s); // s.pop(); // let _x: i32 = s.parse().unwrap(); // ``` // this is actually fine, because the pop call removes the trailing newline. ControlFlow::<(), ()>::Break(()) }); } }