use clippy_utils::consts::{constant, Constant}; use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then}; use clippy_utils::source::snippet_with_context; use clippy_utils::usage::local_used_after_expr; use clippy_utils::visitors::expr_visitor; use clippy_utils::{is_diag_item_method, match_def_path, meets_msrv, msrvs, path_to_local_id, paths}; use if_chain::if_chain; use rustc_errors::Applicability; use rustc_hir::intravisit::Visitor; use rustc_hir::{ BindingAnnotation, Expr, ExprKind, HirId, LangItem, Local, MatchSource, Node, Pat, PatKind, QPath, Stmt, StmtKind, }; use rustc_lint::LateContext; use rustc_middle::ty; use rustc_semver::RustcVersion; use rustc_span::{sym, Span, Symbol, SyntaxContext}; use super::{MANUAL_SPLIT_ONCE, NEEDLESS_SPLITN}; pub(super) fn check( cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>, count: u128, msrv: Option, ) { if count < 2 || !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() { return; } let needless = |usage_kind| match usage_kind { IterUsageKind::Nth(n) => count > n + 1, IterUsageKind::NextTuple => count > 2, }; let manual = count == 2 && meets_msrv(msrv, msrvs::STR_SPLIT_ONCE); match parse_iter_usage(cx, expr.span.ctxt(), cx.tcx.hir().parent_iter(expr.hir_id)) { Some(usage) if needless(usage.kind) => lint_needless(cx, method_name, expr, self_arg, pat_arg), Some(usage) if manual => check_manual_split_once(cx, method_name, expr, self_arg, pat_arg, &usage), None if manual => { check_manual_split_once_indirect(cx, method_name, expr, self_arg, pat_arg); }, _ => {}, } } fn lint_needless(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) { let mut app = Applicability::MachineApplicable; let r = if method_name == "splitn" { "" } else { "r" }; span_lint_and_sugg( cx, NEEDLESS_SPLITN, expr.span, &format!("unnecessary use of `{r}splitn`"), "try this", format!( "{}.{r}split({})", snippet_with_context(cx, self_arg.span, expr.span.ctxt(), "..", &mut app).0, snippet_with_context(cx, pat_arg.span, expr.span.ctxt(), "..", &mut app).0, ), app, ); } fn check_manual_split_once( cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>, usage: &IterUsage, ) { let ctxt = expr.span.ctxt(); let (msg, reverse) = if method_name == "splitn" { ("manual implementation of `split_once`", false) } else { ("manual implementation of `rsplit_once`", true) }; let mut app = Applicability::MachineApplicable; let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0; let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0; let sugg = match usage.kind { IterUsageKind::NextTuple => { if reverse { format!("{self_snip}.rsplit_once({pat_snip}).map(|(x, y)| (y, x))") } else { format!("{self_snip}.split_once({pat_snip})") } }, IterUsageKind::Nth(1) => { let (r, field) = if reverse { ("r", 0) } else { ("", 1) }; match usage.unwrap_kind { Some(UnwrapKind::Unwrap) => { format!("{self_snip}.{r}split_once({pat_snip}).unwrap().{field}") }, Some(UnwrapKind::QuestionMark) => { format!("{self_snip}.{r}split_once({pat_snip})?.{field}") }, None => { format!("{self_snip}.{r}split_once({pat_snip}).map(|x| x.{field})") }, } }, IterUsageKind::Nth(_) => return, }; span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try this", sugg, app); } /// checks for /// /// ``` /// let mut iter = "a.b.c".splitn(2, '.'); /// let a = iter.next(); /// let b = iter.next(); /// ``` fn check_manual_split_once_indirect( cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>, ) -> Option<()> { let ctxt = expr.span.ctxt(); let mut parents = cx.tcx.hir().parent_iter(expr.hir_id); if let (_, Node::Local(local)) = parents.next()? && let PatKind::Binding(BindingAnnotation::MUT, iter_binding_id, iter_ident, None) = local.pat.kind && let (iter_stmt_id, Node::Stmt(_)) = parents.next()? && let (_, Node::Block(enclosing_block)) = parents.next()? && let mut stmts = enclosing_block .stmts .iter() .skip_while(|stmt| stmt.hir_id != iter_stmt_id) .skip(1) && let first = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)? && let second = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)? && first.unwrap_kind == second.unwrap_kind && first.name != second.name && !local_used_after_expr(cx, iter_binding_id, second.init_expr) { let (r, lhs, rhs) = if method_name == "splitn" { ("", first.name, second.name) } else { ("r", second.name, first.name) }; let msg = format!("manual implementation of `{r}split_once`"); let mut app = Applicability::MachineApplicable; let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0; let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0; span_lint_and_then(cx, MANUAL_SPLIT_ONCE, local.span, &msg, |diag| { diag.span_label(first.span, "first usage here"); diag.span_label(second.span, "second usage here"); let unwrap = match first.unwrap_kind { UnwrapKind::Unwrap => ".unwrap()", UnwrapKind::QuestionMark => "?", }; diag.span_suggestion_verbose( local.span, &format!("try `{r}split_once`"), format!("let ({lhs}, {rhs}) = {self_snip}.{r}split_once({pat_snip}){unwrap};"), app, ); let remove_msg = format!("remove the `{iter_ident}` usages"); diag.span_suggestion( first.span, &remove_msg, "", app, ); diag.span_suggestion( second.span, &remove_msg, "", app, ); }); } Some(()) } #[derive(Debug)] struct IndirectUsage<'a> { name: Symbol, span: Span, init_expr: &'a Expr<'a>, unwrap_kind: UnwrapKind, } /// returns `Some(IndirectUsage)` for e.g. /// /// ```ignore /// let name = binding.next()?; /// let name = binding.next().unwrap(); /// ``` fn indirect_usage<'tcx>( cx: &LateContext<'tcx>, stmt: &Stmt<'tcx>, binding: HirId, ctxt: SyntaxContext, ) -> Option> { if let StmtKind::Local(Local { pat: Pat { kind: PatKind::Binding(BindingAnnotation::NONE, _, ident, None), .. }, init: Some(init_expr), hir_id: local_hir_id, .. }) = stmt.kind { let mut path_to_binding = None; expr_visitor(cx, |expr| { if path_to_local_id(expr, binding) { path_to_binding = Some(expr); } path_to_binding.is_none() }) .visit_expr(init_expr); let mut parents = cx.tcx.hir().parent_iter(path_to_binding?.hir_id); let iter_usage = parse_iter_usage(cx, ctxt, &mut parents)?; let (parent_id, _) = parents.find(|(_, node)| { !matches!( node, Node::Expr(Expr { kind: ExprKind::Match(.., MatchSource::TryDesugar), .. }) ) })?; if let IterUsage { kind: IterUsageKind::Nth(0), unwrap_kind: Some(unwrap_kind), .. } = iter_usage { if parent_id == *local_hir_id { return Some(IndirectUsage { name: ident.name, span: stmt.span, init_expr, unwrap_kind, }); } } } None } #[derive(Debug, Clone, Copy)] enum IterUsageKind { Nth(u128), NextTuple, } #[derive(Debug, PartialEq, Eq)] enum UnwrapKind { Unwrap, QuestionMark, } #[derive(Debug)] struct IterUsage { kind: IterUsageKind, unwrap_kind: Option, span: Span, } #[allow(clippy::too_many_lines)] fn parse_iter_usage<'tcx>( cx: &LateContext<'tcx>, ctxt: SyntaxContext, mut iter: impl Iterator)>, ) -> Option { let (kind, span) = match iter.next() { Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => { let (name, args) = if let ExprKind::MethodCall(name, _, [args @ ..], _) = e.kind { (name, args) } else { return None; }; let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?; let iter_id = cx.tcx.get_diagnostic_item(sym::Iterator)?; match (name.ident.as_str(), args) { ("next", []) if cx.tcx.trait_of_item(did) == Some(iter_id) => (IterUsageKind::Nth(0), e.span), ("next_tuple", []) => { return if_chain! { if match_def_path(cx, did, &paths::ITERTOOLS_NEXT_TUPLE); if let ty::Adt(adt_def, subs) = cx.typeck_results().expr_ty(e).kind(); if cx.tcx.is_diagnostic_item(sym::Option, adt_def.did()); if let ty::Tuple(subs) = subs.type_at(0).kind(); if subs.len() == 2; then { Some(IterUsage { kind: IterUsageKind::NextTuple, span: e.span, unwrap_kind: None }) } else { None } }; }, ("nth" | "skip", [idx_expr]) if cx.tcx.trait_of_item(did) == Some(iter_id) => { if let Some((Constant::Int(idx), _)) = constant(cx, cx.typeck_results(), idx_expr) { let span = if name.ident.as_str() == "nth" { e.span } else { if_chain! { if let Some((_, Node::Expr(next_expr))) = iter.next(); if let ExprKind::MethodCall(next_name, _, [], _) = next_expr.kind; if next_name.ident.name == sym::next; if next_expr.span.ctxt() == ctxt; if let Some(next_id) = cx.typeck_results().type_dependent_def_id(next_expr.hir_id); if cx.tcx.trait_of_item(next_id) == Some(iter_id); then { next_expr.span } else { return None; } } }; (IterUsageKind::Nth(idx), span) } else { return None; } }, _ => return None, } }, _ => return None, }; let (unwrap_kind, span) = if let Some((_, Node::Expr(e))) = iter.next() { match e.kind { ExprKind::Call( Expr { kind: ExprKind::Path(QPath::LangItem(LangItem::TryTraitBranch, ..)), .. }, _, ) => { let parent_span = e.span.parent_callsite().unwrap(); if parent_span.ctxt() == ctxt { (Some(UnwrapKind::QuestionMark), parent_span) } else { (None, span) } }, _ if e.span.ctxt() != ctxt => (None, span), ExprKind::MethodCall(name, _, [], _) if name.ident.name == sym::unwrap && cx .typeck_results() .type_dependent_def_id(e.hir_id) .map_or(false, |id| is_diag_item_method(cx, id, sym::Option)) => { (Some(UnwrapKind::Unwrap), e.span) }, _ => (None, span), } } else { (None, span) }; Some(IterUsage { kind, unwrap_kind, span, }) }